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,270 @@
|
|
|
1
|
+
"""Typed build, validation, and safe-clean project operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from ._locking import project_lock
|
|
11
|
+
from ._project_loader import (
|
|
12
|
+
LoadedProject,
|
|
13
|
+
project_execution,
|
|
14
|
+
reject_nested_project_execution,
|
|
15
|
+
)
|
|
16
|
+
from ._project_loader import (
|
|
17
|
+
load_project_model_guarded as load_project_model,
|
|
18
|
+
)
|
|
19
|
+
from ._transaction import promote_directory, recover_promotion, staging_directory
|
|
20
|
+
from .artifacts import ArtifactRecord, artifact_records, validate_export_tree, verify_output_root
|
|
21
|
+
from .determinism import semantic_hash, sha256_file, write_json
|
|
22
|
+
from .exceptions import BuildExportError, ModelValidationError, ProjectConfigError, UnsafePathError
|
|
23
|
+
from .exporters import GlbExporter, IfcExporter, QuantityExporter, StepExporter
|
|
24
|
+
from .exporters.drawings import DrawingExporter
|
|
25
|
+
from .manifests import build_manifest_document, design_manifest, write_preinventory_manifests
|
|
26
|
+
from .project_config import FORMAT_ORDER, load_project_config, output_path, resolve_project_root
|
|
27
|
+
from .validation import ValidationIssue, fatal_issues
|
|
28
|
+
|
|
29
|
+
type FormatName = Literal["step", "ifc", "glb", "drawings", "quantities"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class BuildOptions:
|
|
34
|
+
project_root: Path
|
|
35
|
+
formats: tuple[FormatName, ...] | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ValidationOptions:
|
|
40
|
+
project_root: Path
|
|
41
|
+
formats: tuple[FormatName, ...] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class BuildResult:
|
|
46
|
+
project_root: Path
|
|
47
|
+
output_root: Path
|
|
48
|
+
formats: tuple[FormatName, ...]
|
|
49
|
+
artifacts: tuple[ArtifactRecord, ...]
|
|
50
|
+
build_manifest: Path
|
|
51
|
+
source_set_hash: str
|
|
52
|
+
design_semantic_hash: str
|
|
53
|
+
artifact_set_hash: str
|
|
54
|
+
stable_artifact_set_hash: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class ValidationReport:
|
|
59
|
+
project_root: Path
|
|
60
|
+
formats: tuple[FormatName, ...]
|
|
61
|
+
issues: tuple[ValidationIssue, ...]
|
|
62
|
+
source_set_hash: str
|
|
63
|
+
design_semantic_hash: str
|
|
64
|
+
annotation_provider_id: str | None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def ok(self) -> bool:
|
|
68
|
+
return not fatal_issues(self.issues)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
EXPORTERS: dict[str, type[Any]] = {
|
|
72
|
+
"step": StepExporter,
|
|
73
|
+
"ifc": IfcExporter,
|
|
74
|
+
"glb": GlbExporter,
|
|
75
|
+
"drawings": DrawingExporter,
|
|
76
|
+
"quantities": QuantityExporter,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_project(options: BuildOptions) -> BuildResult:
|
|
81
|
+
if not isinstance(options, BuildOptions):
|
|
82
|
+
raise TypeError("build_project expects BuildOptions")
|
|
83
|
+
requested = _validate_requested_formats(options.formats)
|
|
84
|
+
root = resolve_project_root(options.project_root)
|
|
85
|
+
with project_execution():
|
|
86
|
+
with project_lock(root):
|
|
87
|
+
config = load_project_config(root)
|
|
88
|
+
formats = config.default_formats if requested is None else requested
|
|
89
|
+
destination = output_path(root, config)
|
|
90
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
recover_promotion(destination, lambda candidate: verify_output_root(candidate))
|
|
92
|
+
loaded = load_project_model(root, include_drawings="drawings" in formats)
|
|
93
|
+
_raise_fatal_model_issues(loaded.issues)
|
|
94
|
+
source_inputs, source_set_hash = _source_records(loaded)
|
|
95
|
+
staging = staging_directory(destination)
|
|
96
|
+
try:
|
|
97
|
+
staging.mkdir()
|
|
98
|
+
if (destination / ".gitkeep").is_file() and not (destination / ".gitkeep").is_symlink():
|
|
99
|
+
(staging / ".gitkeep").touch()
|
|
100
|
+
for name in formats:
|
|
101
|
+
try:
|
|
102
|
+
if name == "drawings":
|
|
103
|
+
DrawingExporter().export(
|
|
104
|
+
loaded.model,
|
|
105
|
+
staging,
|
|
106
|
+
loaded.drawing_sheets,
|
|
107
|
+
loaded.annotations,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
EXPORTERS[name]().export(loaded.model, staging)
|
|
111
|
+
except (BuildExportError, ImportError):
|
|
112
|
+
raise
|
|
113
|
+
except Exception as error:
|
|
114
|
+
raise BuildExportError(f"{name} exporter failed: {error}") from error
|
|
115
|
+
_, _, design = write_preinventory_manifests(loaded.model, staging, formats)
|
|
116
|
+
validate_export_tree(staging, formats, loaded.model.artifact_stem)
|
|
117
|
+
records = artifact_records(staging)
|
|
118
|
+
artifact_set_hash = semantic_hash([_record_dict(record) for record in records])
|
|
119
|
+
stable_artifact_set_hash = semantic_hash(
|
|
120
|
+
[_record_dict(record) for record in records if not record.volatile]
|
|
121
|
+
)
|
|
122
|
+
manifest = build_manifest_document(
|
|
123
|
+
project_name=root.name,
|
|
124
|
+
source_revision=loaded.source_revision,
|
|
125
|
+
source_dirty=loaded.source_dirty,
|
|
126
|
+
source_inputs=source_inputs,
|
|
127
|
+
source_set_hash=source_set_hash,
|
|
128
|
+
config_schema_version=loaded.project_config.schema_version,
|
|
129
|
+
selected_formats=formats,
|
|
130
|
+
annotation_import=loaded.project_config.drawing_annotations_target,
|
|
131
|
+
annotation_provider_id=(
|
|
132
|
+
None if loaded.annotations is None else loaded.annotations.provider_id
|
|
133
|
+
),
|
|
134
|
+
artifacts=records,
|
|
135
|
+
design_semantic_hash=design["semantic_hash"],
|
|
136
|
+
artifact_set_hash=artifact_set_hash,
|
|
137
|
+
stable_artifact_set_hash=stable_artifact_set_hash,
|
|
138
|
+
issues=loaded.issues,
|
|
139
|
+
)
|
|
140
|
+
write_json(staging / "manifests/build-manifest.json", manifest)
|
|
141
|
+
verify_output_root(staging, root)
|
|
142
|
+
promote_directory(staging, destination, lambda candidate: verify_output_root(candidate, root))
|
|
143
|
+
finally:
|
|
144
|
+
if staging.exists():
|
|
145
|
+
shutil.rmtree(staging)
|
|
146
|
+
verified = verify_output_root(destination, root)
|
|
147
|
+
return BuildResult(
|
|
148
|
+
project_root=root,
|
|
149
|
+
output_root=destination,
|
|
150
|
+
formats=formats,
|
|
151
|
+
artifacts=verified.artifacts,
|
|
152
|
+
build_manifest=verified.build_manifest,
|
|
153
|
+
source_set_hash=source_set_hash,
|
|
154
|
+
design_semantic_hash=design["semantic_hash"],
|
|
155
|
+
artifact_set_hash=verified.artifact_set_hash,
|
|
156
|
+
stable_artifact_set_hash=verified.stable_artifact_set_hash,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_project(options: ValidationOptions) -> ValidationReport:
|
|
161
|
+
if not isinstance(options, ValidationOptions):
|
|
162
|
+
raise TypeError("validate_project expects ValidationOptions")
|
|
163
|
+
requested = _validate_requested_formats(options.formats)
|
|
164
|
+
root = resolve_project_root(options.project_root)
|
|
165
|
+
with project_execution():
|
|
166
|
+
with project_lock(root):
|
|
167
|
+
config = load_project_config(root)
|
|
168
|
+
formats = config.default_formats if requested is None else requested
|
|
169
|
+
loaded = load_project_model(root, include_drawings="drawings" in formats)
|
|
170
|
+
_, source_set_hash = _source_records(loaded)
|
|
171
|
+
return ValidationReport(
|
|
172
|
+
project_root=loaded.project_root,
|
|
173
|
+
formats=formats,
|
|
174
|
+
issues=loaded.issues,
|
|
175
|
+
source_set_hash=source_set_hash,
|
|
176
|
+
design_semantic_hash=design_manifest(loaded.model)["semantic_hash"],
|
|
177
|
+
annotation_provider_id=(
|
|
178
|
+
None if loaded.annotations is None else loaded.annotations.provider_id
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def clean_project(project_root: Path) -> None:
|
|
184
|
+
reject_nested_project_execution()
|
|
185
|
+
root = resolve_project_root(project_root)
|
|
186
|
+
config = load_project_config(root)
|
|
187
|
+
generated = output_path(root, config)
|
|
188
|
+
with project_lock(root):
|
|
189
|
+
recover_promotion(generated, lambda candidate: verify_output_root(candidate))
|
|
190
|
+
_validate_clean_target(root, generated)
|
|
191
|
+
if not generated.exists():
|
|
192
|
+
return
|
|
193
|
+
anchor = generated / ".gitkeep"
|
|
194
|
+
keep_anchor = anchor.is_file() and not anchor.is_symlink()
|
|
195
|
+
for path in generated.rglob("*"):
|
|
196
|
+
if path.is_symlink():
|
|
197
|
+
raise UnsafePathError(f"Refusing to clean an output tree containing a symlink: {path}")
|
|
198
|
+
if keep_anchor:
|
|
199
|
+
for child in generated.iterdir():
|
|
200
|
+
if child == anchor:
|
|
201
|
+
continue
|
|
202
|
+
if child.is_dir():
|
|
203
|
+
shutil.rmtree(child)
|
|
204
|
+
else:
|
|
205
|
+
child.unlink()
|
|
206
|
+
else:
|
|
207
|
+
shutil.rmtree(generated)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _validate_requested_formats(requested: tuple[FormatName, ...] | None) -> tuple[FormatName, ...] | None:
|
|
211
|
+
if requested is None:
|
|
212
|
+
return None
|
|
213
|
+
if not isinstance(requested, tuple) or not requested:
|
|
214
|
+
raise ProjectConfigError("formats must be None or a nonempty tuple")
|
|
215
|
+
if not all(isinstance(item, str) for item in requested):
|
|
216
|
+
raise ProjectConfigError("formats must contain only format names")
|
|
217
|
+
if len(requested) != len(set(requested)):
|
|
218
|
+
raise ProjectConfigError("formats contains a duplicate")
|
|
219
|
+
unknown = sorted(set(requested) - set(FORMAT_ORDER))
|
|
220
|
+
if unknown:
|
|
221
|
+
raise ProjectConfigError(f"Unsupported format(s): {', '.join(unknown)}")
|
|
222
|
+
selected = set(requested)
|
|
223
|
+
return tuple(name for name in FORMAT_ORDER if name in selected)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _raise_fatal_model_issues(issues: tuple[ValidationIssue, ...]) -> None:
|
|
227
|
+
fatals = fatal_issues(issues)
|
|
228
|
+
if fatals:
|
|
229
|
+
details = "; ".join(f"{issue.code}: {issue.message} ({issue.element_id or 'model'})" for issue in fatals)
|
|
230
|
+
raise ModelValidationError(f"Model validation failed: {details}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _source_records(loaded: LoadedProject) -> tuple[list[dict[str, Any]], str]:
|
|
234
|
+
records = []
|
|
235
|
+
for relative in sorted(loaded.project_config.source_inputs):
|
|
236
|
+
path = loaded.project_root / relative
|
|
237
|
+
records.append({"path": relative, "size": path.stat().st_size, "sha256": sha256_file(path)})
|
|
238
|
+
return records, semantic_hash(records)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _record_dict(record: ArtifactRecord) -> dict[str, Any]:
|
|
242
|
+
return {
|
|
243
|
+
"path": record.path,
|
|
244
|
+
"size": record.size,
|
|
245
|
+
"sha256": record.sha256,
|
|
246
|
+
"media_type": record.media_type,
|
|
247
|
+
"producer": record.producer,
|
|
248
|
+
"schema_id": record.schema_id,
|
|
249
|
+
"schema_version": record.schema_version,
|
|
250
|
+
"volatile": record.volatile,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _validate_clean_target(root: Path, generated: Path) -> None:
|
|
255
|
+
if generated.is_symlink() or not generated.resolve(strict=False).is_relative_to(root):
|
|
256
|
+
raise UnsafePathError(f"Refusing to clean unsafe output directory: {generated}")
|
|
257
|
+
if generated.exists() and not generated.is_dir():
|
|
258
|
+
raise UnsafePathError(f"Configured output path is not a directory: {generated}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = [
|
|
262
|
+
"BuildOptions",
|
|
263
|
+
"BuildResult",
|
|
264
|
+
"FormatName",
|
|
265
|
+
"ValidationOptions",
|
|
266
|
+
"ValidationReport",
|
|
267
|
+
"build_project",
|
|
268
|
+
"clean_project",
|
|
269
|
+
"validate_project",
|
|
270
|
+
]
|
python_cad_tools/cli.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Installed ``python-cad`` command for project operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
ArtifactIntegrityError,
|
|
17
|
+
BuildExportError,
|
|
18
|
+
BuildLockedError,
|
|
19
|
+
ModelValidationError,
|
|
20
|
+
ProjectConfigError,
|
|
21
|
+
SiteError,
|
|
22
|
+
UnsafePathError,
|
|
23
|
+
)
|
|
24
|
+
from .project_config import FORMAT_ORDER, resolve_project_root
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parser() -> argparse.ArgumentParser:
|
|
28
|
+
value = argparse.ArgumentParser(
|
|
29
|
+
prog="python-cad",
|
|
30
|
+
description="Validate, build, verify, and clean Python CAD Tools projects.",
|
|
31
|
+
)
|
|
32
|
+
value.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
33
|
+
value.add_argument("--verbose", action="store_true", help="Include a traceback for unexpected failures.")
|
|
34
|
+
commands = value.add_subparsers(dest="command", required=True)
|
|
35
|
+
for name in ("validate", "build"):
|
|
36
|
+
operation = commands.add_parser(name, help=f"{name.title()} the configured project.")
|
|
37
|
+
_project_root_argument(operation)
|
|
38
|
+
operation.add_argument(
|
|
39
|
+
"--format",
|
|
40
|
+
action="append",
|
|
41
|
+
choices=FORMAT_ORDER,
|
|
42
|
+
dest="formats",
|
|
43
|
+
help="Select one output format; repeat for several.",
|
|
44
|
+
)
|
|
45
|
+
verify = commands.add_parser("verify", help="Verify final bytes, schemas, and parsability.")
|
|
46
|
+
_project_root_argument(verify)
|
|
47
|
+
clean = commands.add_parser("clean", help="Safely remove the configured generated output.")
|
|
48
|
+
_project_root_argument(clean)
|
|
49
|
+
init = commands.add_parser("init", help="Create a generic Python CAD project scaffold.")
|
|
50
|
+
init.add_argument("--destination", type=Path, required=True, help="Destination project directory.")
|
|
51
|
+
init.add_argument("--force", action="store_true", help="Allow a non-empty destination; existing scaffold files are overwritten.")
|
|
52
|
+
prepare_site = commands.add_parser("prepare-site", help="Prepare a verified, Node-free viewer site.")
|
|
53
|
+
_project_root_argument(prepare_site)
|
|
54
|
+
prepare_site.add_argument("--destination", type=Path, required=True, help="Managed site destination.")
|
|
55
|
+
prepare_site.add_argument("--base-path", default="/", help="Deployment URL base path; defaults to /.")
|
|
56
|
+
serve = commands.add_parser("serve", help="Prepare and serve a verified viewer site.")
|
|
57
|
+
_project_root_argument(serve)
|
|
58
|
+
serve.add_argument("--host", default="127.0.0.1", help="Bind host; defaults to loopback.")
|
|
59
|
+
serve.add_argument("--port", type=int, default=8000, help="Bind port; use 0 for an ephemeral port.")
|
|
60
|
+
serve.add_argument("--build", action="store_true", help="Build the project before serving.")
|
|
61
|
+
serve.add_argument("--allow-unsafe-bind", action="store_true", help="Explicitly allow a non-loopback bind.")
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main(argv: list[str] | None = None, project_dir: Path | None = None) -> int:
|
|
66
|
+
args = parser().parse_args(argv)
|
|
67
|
+
try:
|
|
68
|
+
from .artifacts import verify_build_output
|
|
69
|
+
from .build import BuildOptions, ValidationOptions, build_project, clean_project, validate_project
|
|
70
|
+
from .site import prepare_site, serve_project
|
|
71
|
+
|
|
72
|
+
_configure_logging()
|
|
73
|
+
if args.command == "init":
|
|
74
|
+
from ._scaffold import scaffold_project
|
|
75
|
+
|
|
76
|
+
result = scaffold_project(args.destination, force=args.force)
|
|
77
|
+
_print_json(
|
|
78
|
+
{
|
|
79
|
+
"status": "ok",
|
|
80
|
+
"operation": "init",
|
|
81
|
+
"destination": str(result.destination),
|
|
82
|
+
"created_files": list(result.created_files),
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
return 0
|
|
86
|
+
root = resolve_project_root(project_dir if project_dir is not None else args.project_root)
|
|
87
|
+
if args.command == "serve":
|
|
88
|
+
serve_result = serve_project(
|
|
89
|
+
root,
|
|
90
|
+
host=args.host,
|
|
91
|
+
port=args.port,
|
|
92
|
+
build=args.build,
|
|
93
|
+
allow_unsafe_bind=args.allow_unsafe_bind,
|
|
94
|
+
)
|
|
95
|
+
return 130 if serve_result.interrupted else 0
|
|
96
|
+
if args.command == "prepare-site":
|
|
97
|
+
site_result = prepare_site(root, args.destination, args.base_path)
|
|
98
|
+
_print_json(
|
|
99
|
+
{
|
|
100
|
+
"status": "ok",
|
|
101
|
+
"operation": "prepare-site",
|
|
102
|
+
"destination": str(site_result.destination),
|
|
103
|
+
"base_path": site_result.base_path,
|
|
104
|
+
"design_build_hash": site_result.design_build_hash,
|
|
105
|
+
"file_count": site_result.file_count,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
elif args.command == "clean":
|
|
109
|
+
clean_project(root)
|
|
110
|
+
_print_json({"status": "ok", "operation": "clean", "project_root": str(root)})
|
|
111
|
+
elif args.command == "verify":
|
|
112
|
+
verification = verify_build_output(root)
|
|
113
|
+
_print_json(
|
|
114
|
+
{
|
|
115
|
+
"status": "ok",
|
|
116
|
+
"operation": "verify",
|
|
117
|
+
"artifact_count": len(verification.artifacts),
|
|
118
|
+
"stable_artifact_set_hash": verification.stable_artifact_set_hash,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
elif args.command == "validate":
|
|
122
|
+
formats = None if args.formats is None else tuple(args.formats)
|
|
123
|
+
report = validate_project(ValidationOptions(root, formats))
|
|
124
|
+
errors = [issue for issue in report.issues if issue.severity == "error"]
|
|
125
|
+
if errors:
|
|
126
|
+
raise ModelValidationError("; ".join(f"{issue.code}: {issue.message}" for issue in errors))
|
|
127
|
+
_print_json(
|
|
128
|
+
{
|
|
129
|
+
"status": "ok",
|
|
130
|
+
"operation": "validate",
|
|
131
|
+
"formats": list(report.formats),
|
|
132
|
+
"warning_count": len(report.issues),
|
|
133
|
+
"design_semantic_hash": report.design_semantic_hash,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
formats = None if args.formats is None else tuple(args.formats)
|
|
138
|
+
build_result = build_project(BuildOptions(root, formats))
|
|
139
|
+
_print_json(
|
|
140
|
+
{
|
|
141
|
+
"status": "ok",
|
|
142
|
+
"operation": "build",
|
|
143
|
+
"artifact_count": len(build_result.artifacts),
|
|
144
|
+
"output_root": str(build_result.output_root),
|
|
145
|
+
"stable_artifact_set_hash": build_result.stable_artifact_set_hash,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
return 0
|
|
149
|
+
except KeyboardInterrupt:
|
|
150
|
+
_print_error("interrupted", "Operation interrupted")
|
|
151
|
+
return 130
|
|
152
|
+
except BaseException as error:
|
|
153
|
+
code, name = _error_code(error)
|
|
154
|
+
_print_error(name, _concise_message(error))
|
|
155
|
+
if getattr(args, "verbose", False) and code == 1:
|
|
156
|
+
traceback.print_exc()
|
|
157
|
+
return code
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _project_root_argument(command: argparse.ArgumentParser) -> None:
|
|
161
|
+
command.add_argument("--project-root", type=Path, default=None, help="Project directory; defaults to the current directory.")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _error_code(error: BaseException) -> tuple[int, str]:
|
|
165
|
+
if isinstance(error, (ProjectConfigError, argparse.ArgumentError)):
|
|
166
|
+
return 2, "project_config"
|
|
167
|
+
if isinstance(error, ModelValidationError):
|
|
168
|
+
return 3, "model_validation"
|
|
169
|
+
if isinstance(error, (BuildExportError, ImportError)):
|
|
170
|
+
return 4, "build_export"
|
|
171
|
+
if isinstance(error, ArtifactIntegrityError):
|
|
172
|
+
return 5, "artifact_integrity"
|
|
173
|
+
if isinstance(error, (UnsafePathError, BuildLockedError)):
|
|
174
|
+
return 6, "unsafe_or_locked"
|
|
175
|
+
if isinstance(error, SiteError):
|
|
176
|
+
return 7, "site"
|
|
177
|
+
return 1, "internal"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _concise_message(error: BaseException) -> str:
|
|
181
|
+
message = str(error).strip() or type(error).__name__
|
|
182
|
+
return " ".join(message.splitlines())
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _print_json(value: dict[str, Any]) -> None:
|
|
186
|
+
print(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _print_error(code: str, message: str) -> None:
|
|
190
|
+
print(
|
|
191
|
+
json.dumps(
|
|
192
|
+
{"error": {"code": code, "message": message}, "status": "error"},
|
|
193
|
+
sort_keys=True,
|
|
194
|
+
separators=(",", ":"),
|
|
195
|
+
ensure_ascii=False,
|
|
196
|
+
),
|
|
197
|
+
file=sys.stderr,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _configure_logging() -> None:
|
|
202
|
+
value = os.environ.get("PYTHON_CAD_LOG_LEVEL", "WARNING").upper()
|
|
203
|
+
levels = {
|
|
204
|
+
"CRITICAL": logging.CRITICAL,
|
|
205
|
+
"ERROR": logging.ERROR,
|
|
206
|
+
"WARNING": logging.WARNING,
|
|
207
|
+
"INFO": logging.INFO,
|
|
208
|
+
"DEBUG": logging.DEBUG,
|
|
209
|
+
}
|
|
210
|
+
if value not in levels:
|
|
211
|
+
raise ProjectConfigError("PYTHON_CAD_LOG_LEVEL must be CRITICAL, ERROR, WARNING, INFO, or DEBUG")
|
|
212
|
+
logging.getLogger("python_cad_tools").setLevel(levels[value])
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Immutable contexts passed to trusted project entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TextIO, overload
|
|
8
|
+
|
|
9
|
+
from ._project_state import open_authoritative_source
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .drawings import DrawingSheet
|
|
13
|
+
from .elements import DesignModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class BuildContext:
|
|
18
|
+
project_root: Path
|
|
19
|
+
config: Any
|
|
20
|
+
source_revision: str | None
|
|
21
|
+
source_dirty: bool
|
|
22
|
+
|
|
23
|
+
def source_path(self, relative_path: str | Path) -> Path:
|
|
24
|
+
"""Return and record a declared authoritative project file."""
|
|
25
|
+
stream = open_authoritative_source(self.project_root, relative_path, "rb", None)
|
|
26
|
+
try:
|
|
27
|
+
return Path(stream.name)
|
|
28
|
+
finally:
|
|
29
|
+
stream.close()
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def open_source(self, relative_path: str | Path, mode: Literal["r"] = "r", *, encoding: str = "utf-8") -> TextIO: ...
|
|
33
|
+
|
|
34
|
+
@overload
|
|
35
|
+
def open_source(self, relative_path: str | Path, mode: Literal["rb"], *, encoding: None = None) -> BinaryIO: ...
|
|
36
|
+
|
|
37
|
+
def open_source(
|
|
38
|
+
self,
|
|
39
|
+
relative_path: str | Path,
|
|
40
|
+
mode: Literal["r", "rb"] = "r",
|
|
41
|
+
*,
|
|
42
|
+
encoding: str | None = "utf-8",
|
|
43
|
+
) -> TextIO | BinaryIO:
|
|
44
|
+
"""Open and record a declared authoritative source in read-only mode."""
|
|
45
|
+
return open_authoritative_source(self.project_root, relative_path, mode, encoding)
|
|
46
|
+
|
|
47
|
+
def read_text(self, relative_path: str | Path, *, encoding: str = "utf-8") -> str:
|
|
48
|
+
"""Read and record a declared authoritative UTF-8 source file."""
|
|
49
|
+
with self.open_source(relative_path, "r", encoding=encoding) as stream:
|
|
50
|
+
return stream.read()
|
|
51
|
+
|
|
52
|
+
def read_bytes(self, relative_path: str | Path) -> bytes:
|
|
53
|
+
"""Read and record a declared authoritative binary source file."""
|
|
54
|
+
with self.open_source(relative_path, "rb", encoding=None) as stream:
|
|
55
|
+
return stream.read()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class DrawingContext:
|
|
60
|
+
project_root: Path
|
|
61
|
+
config: Any
|
|
62
|
+
model: DesignModel
|
|
63
|
+
sheets: tuple[DrawingSheet, ...]
|
|
64
|
+
length_unit: Literal["mm"] = "mm"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = ["BuildContext", "DrawingContext"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Deterministic serialization and hashing helpers for Python CAD Tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .exceptions import ProjectConfigError
|
|
13
|
+
|
|
14
|
+
DEFAULT_BUILD_EPOCH = datetime(1980, 1, 1, tzinfo=UTC)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def canonical_json_bytes(value: Any) -> bytes:
|
|
18
|
+
return (
|
|
19
|
+
json.dumps(
|
|
20
|
+
value,
|
|
21
|
+
sort_keys=True,
|
|
22
|
+
separators=(",", ":"),
|
|
23
|
+
ensure_ascii=False,
|
|
24
|
+
allow_nan=False,
|
|
25
|
+
)
|
|
26
|
+
+ "\n"
|
|
27
|
+
).encode("utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_json(path: Path, value: Any) -> Path:
|
|
31
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
path.write_bytes(canonical_json_bytes(value))
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def sha256_bytes(value: bytes) -> str:
|
|
37
|
+
return hashlib.sha256(value).hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sha256_file(path: Path) -> str:
|
|
41
|
+
digest = hashlib.sha256()
|
|
42
|
+
with path.open("rb") as stream:
|
|
43
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
44
|
+
digest.update(chunk)
|
|
45
|
+
return digest.hexdigest()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def semantic_hash(value: Any) -> str:
|
|
49
|
+
return sha256_bytes(canonical_json_bytes(value))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalized_build_epoch() -> datetime:
|
|
53
|
+
"""Return the validated reproducible-build epoch for deterministic writers."""
|
|
54
|
+
raw = os.environ.get("SOURCE_DATE_EPOCH")
|
|
55
|
+
if raw is None:
|
|
56
|
+
return DEFAULT_BUILD_EPOCH
|
|
57
|
+
if not raw.isascii() or not raw.isdigit():
|
|
58
|
+
raise ProjectConfigError("SOURCE_DATE_EPOCH must be a nonnegative integer Unix timestamp")
|
|
59
|
+
try:
|
|
60
|
+
return datetime.fromtimestamp(int(raw), UTC)
|
|
61
|
+
except (OverflowError, OSError, ValueError) as error:
|
|
62
|
+
raise ProjectConfigError("SOURCE_DATE_EPOCH is outside the supported timestamp range") from error
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalized_epoch_iso() -> str:
|
|
66
|
+
return normalized_build_epoch().isoformat().replace("+00:00", "Z")
|