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,338 @@
|
|
|
1
|
+
"""Schema-versioned manifest construction and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .determinism import normalized_epoch_iso, semantic_hash, write_json
|
|
15
|
+
from .exceptions import ArtifactIntegrityError
|
|
16
|
+
from .geometry import bounds
|
|
17
|
+
|
|
18
|
+
SCHEMAS: dict[str, tuple[int, frozenset[str]]] = {
|
|
19
|
+
"urn:python-cad-tools:schema:design-manifest:1": (
|
|
20
|
+
1,
|
|
21
|
+
frozenset({"model_id", "model_name", "artifact_stem", "coordinate_system", "metadata", "elements", "semantic_hash"}),
|
|
22
|
+
),
|
|
23
|
+
"urn:python-cad-tools:schema:build-manifest:1": (
|
|
24
|
+
1,
|
|
25
|
+
frozenset(
|
|
26
|
+
{
|
|
27
|
+
"producer",
|
|
28
|
+
"project",
|
|
29
|
+
"source_inputs",
|
|
30
|
+
"source_set_hash",
|
|
31
|
+
"config_schema_version",
|
|
32
|
+
"selected_formats",
|
|
33
|
+
"annotation_import",
|
|
34
|
+
"annotation_provider_id",
|
|
35
|
+
"emitted_schemas",
|
|
36
|
+
"artifacts",
|
|
37
|
+
"design_semantic_hash",
|
|
38
|
+
"artifact_set_hash",
|
|
39
|
+
"stable_artifact_set_hash",
|
|
40
|
+
"validation",
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
"urn:python-cad-tools:schema:run-metadata:1": (
|
|
45
|
+
1,
|
|
46
|
+
frozenset({"started_at", "normalized_epoch", "host", "platform", "python", "process_id"}),
|
|
47
|
+
),
|
|
48
|
+
"urn:python-cad-tools:schema:glb-manifest:1": (
|
|
49
|
+
1,
|
|
50
|
+
frozenset({"coordinate_transform", "elements", "triangle_counts", "bounds_cad_mm", "bounds_y_up_mm", "file_size"}),
|
|
51
|
+
),
|
|
52
|
+
"urn:python-cad-tools:schema:step-validation:1": (
|
|
53
|
+
1,
|
|
54
|
+
frozenset({"valid", "schema", "requested_schema", "schema_note", "solid_count", "bounds_mm"}),
|
|
55
|
+
),
|
|
56
|
+
"urn:python-cad-tools:schema:ifc-validation:1": (
|
|
57
|
+
1,
|
|
58
|
+
frozenset({"schema", "valid", "fatal_count", "issue_count", "issues", "bounds_mm", "element_global_ids"}),
|
|
59
|
+
),
|
|
60
|
+
"urn:python-cad-tools:schema:quantities:1": (1, frozenset({"records"})),
|
|
61
|
+
"urn:python-cad-tools:schema:drawing-annotations:1": (
|
|
62
|
+
1,
|
|
63
|
+
frozenset({"provider_id", "annotations", "sheets"}),
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
_SNAKE_CASE = re.compile(r"[a-z][a-z0-9]*(?:_[a-z0-9]+)*\Z")
|
|
67
|
+
_DRAWING_ID = re.compile(r"[A-Za-z0-9._-]+\Z", re.ASCII)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def design_manifest(model: Any) -> dict[str, Any]:
|
|
71
|
+
elements = []
|
|
72
|
+
for element in model.walk():
|
|
73
|
+
elements.append(
|
|
74
|
+
{
|
|
75
|
+
"id": element.id,
|
|
76
|
+
"name": element.name,
|
|
77
|
+
"category": element.category,
|
|
78
|
+
"geometry_kind": element.geometry_kind,
|
|
79
|
+
"physical": element.physical,
|
|
80
|
+
"bounds_mm": [round(value, 6) for value in bounds(element.geometry)],
|
|
81
|
+
"placement": {
|
|
82
|
+
"translation_mm": element.placement.translation_mm,
|
|
83
|
+
"rotation_deg_xyz": element.placement.rotation_deg_xyz,
|
|
84
|
+
},
|
|
85
|
+
"dimensions": {
|
|
86
|
+
"length_mm": element.dimensions.length_mm,
|
|
87
|
+
"width_mm": element.dimensions.width_mm,
|
|
88
|
+
"height_mm": element.dimensions.height_mm,
|
|
89
|
+
"radius_mm": element.dimensions.radius_mm,
|
|
90
|
+
"extras": element.dimensions.extras,
|
|
91
|
+
},
|
|
92
|
+
"material_id": element.material.id if element.material else None,
|
|
93
|
+
"color_rgb": element.color_rgb,
|
|
94
|
+
"ifc_class": element.ifc_mapping.ifc_class,
|
|
95
|
+
"ifc_exclusion": element.ifc_mapping.exclude_reason,
|
|
96
|
+
"storey": element.storey,
|
|
97
|
+
"tags": sorted(element.tags),
|
|
98
|
+
"parent_id": element.parent_id,
|
|
99
|
+
"source_module": element.source_module,
|
|
100
|
+
"export_formats": sorted(element.export_formats),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
value: dict[str, Any] = {
|
|
104
|
+
"schema_id": "urn:python-cad-tools:schema:design-manifest:1",
|
|
105
|
+
"schema_version": 1,
|
|
106
|
+
"model_id": model.id,
|
|
107
|
+
"model_name": model.name,
|
|
108
|
+
"artifact_stem": model.artifact_stem,
|
|
109
|
+
"coordinate_system": model.coordinate_system,
|
|
110
|
+
"metadata": model.metadata,
|
|
111
|
+
"elements": elements,
|
|
112
|
+
}
|
|
113
|
+
value["semantic_hash"] = semantic_hash(value)
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def write_preinventory_manifests(
|
|
118
|
+
model: Any,
|
|
119
|
+
staging: Path,
|
|
120
|
+
selected_formats: tuple[str, ...],
|
|
121
|
+
) -> tuple[Path, Path, dict[str, Any]]:
|
|
122
|
+
target = staging / "manifests"
|
|
123
|
+
design = design_manifest(model)
|
|
124
|
+
design_path = write_json(target / "design-manifest.json", design)
|
|
125
|
+
run_path = write_json(
|
|
126
|
+
target / "run-metadata.json",
|
|
127
|
+
{
|
|
128
|
+
"schema_id": "urn:python-cad-tools:schema:run-metadata:1",
|
|
129
|
+
"schema_version": 1,
|
|
130
|
+
"started_at": datetime.now(UTC).isoformat(),
|
|
131
|
+
"normalized_epoch": normalized_epoch_iso(),
|
|
132
|
+
"host": platform.node(),
|
|
133
|
+
"platform": platform.platform(),
|
|
134
|
+
"python": platform.python_version(),
|
|
135
|
+
"process_id": __import__("os").getpid(),
|
|
136
|
+
"selected_formats": list(selected_formats),
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
return design_path, run_path, design
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_manifest_document(
|
|
143
|
+
*,
|
|
144
|
+
project_name: str,
|
|
145
|
+
source_revision: str | None,
|
|
146
|
+
source_dirty: bool,
|
|
147
|
+
source_inputs: list[dict[str, Any]],
|
|
148
|
+
source_set_hash: str,
|
|
149
|
+
config_schema_version: int,
|
|
150
|
+
selected_formats: tuple[str, ...],
|
|
151
|
+
annotation_import: str | None,
|
|
152
|
+
annotation_provider_id: str | None,
|
|
153
|
+
artifacts: tuple[Any, ...],
|
|
154
|
+
design_semantic_hash: str,
|
|
155
|
+
artifact_set_hash: str,
|
|
156
|
+
stable_artifact_set_hash: str,
|
|
157
|
+
issues: tuple[Any, ...],
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
emitted_schemas = {
|
|
160
|
+
record.path: {"schema_id": record.schema_id, "schema_version": record.schema_version}
|
|
161
|
+
for record in artifacts
|
|
162
|
+
if record.schema_id is not None
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
"schema_id": "urn:python-cad-tools:schema:build-manifest:1",
|
|
166
|
+
"schema_version": 1,
|
|
167
|
+
"producer": {"distribution": "python-cad-tools", "version": __version__},
|
|
168
|
+
"project": {"repository": project_name, "revision": source_revision, "dirty": source_dirty},
|
|
169
|
+
"source_inputs": source_inputs,
|
|
170
|
+
"source_set_hash": source_set_hash,
|
|
171
|
+
"config_schema_version": config_schema_version,
|
|
172
|
+
"selected_formats": list(selected_formats),
|
|
173
|
+
"annotation_import": annotation_import,
|
|
174
|
+
"annotation_provider_id": annotation_provider_id,
|
|
175
|
+
"emitted_schemas": emitted_schemas,
|
|
176
|
+
"artifacts": [asdict(record) for record in artifacts],
|
|
177
|
+
"design_semantic_hash": design_semantic_hash,
|
|
178
|
+
"artifact_set_hash": artifact_set_hash,
|
|
179
|
+
"stable_artifact_set_hash": stable_artifact_set_hash,
|
|
180
|
+
"validation": {
|
|
181
|
+
"status": "passed",
|
|
182
|
+
"warnings": [asdict(issue) for issue in issues if issue.severity == "warning"],
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def load_json_document(path: Path) -> dict[str, Any]:
|
|
188
|
+
try:
|
|
189
|
+
value = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=_unique_object)
|
|
190
|
+
except (OSError, UnicodeError, json.JSONDecodeError, ArtifactIntegrityError) as error:
|
|
191
|
+
if isinstance(error, ArtifactIntegrityError):
|
|
192
|
+
raise
|
|
193
|
+
raise ArtifactIntegrityError(f"Unable to parse JSON document {path}: {error}") from error
|
|
194
|
+
if not isinstance(value, dict):
|
|
195
|
+
raise ArtifactIntegrityError(f"Package JSON document must contain an object: {path}")
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate_json_document(path: Path) -> dict[str, Any]:
|
|
200
|
+
value = load_json_document(path)
|
|
201
|
+
schema_id = value.get("schema_id")
|
|
202
|
+
schema_version = value.get("schema_version")
|
|
203
|
+
if not isinstance(schema_id, str) or schema_id not in SCHEMAS:
|
|
204
|
+
raise ArtifactIntegrityError(f"Unsupported or missing schema_id in {path}")
|
|
205
|
+
expected_version, required = SCHEMAS[schema_id]
|
|
206
|
+
if type(schema_version) is not int or schema_version != expected_version:
|
|
207
|
+
raise ArtifactIntegrityError(f"Unsupported schema_version in {path}: {schema_version!r}")
|
|
208
|
+
missing = sorted(required - set(value))
|
|
209
|
+
if missing:
|
|
210
|
+
raise ArtifactIntegrityError(f"Schema document {path} is missing: {', '.join(missing)}")
|
|
211
|
+
_validate_package_keys(value, path)
|
|
212
|
+
if schema_id == "urn:python-cad-tools:schema:drawing-annotations:1":
|
|
213
|
+
_validate_drawing_annotation_document(value, path)
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def stable_build_manifest_projection(document: dict[str, Any]) -> dict[str, Any]:
|
|
218
|
+
projected = dict(document)
|
|
219
|
+
projected.pop("artifact_set_hash", None)
|
|
220
|
+
projected["artifacts"] = [record for record in document["artifacts"] if not record["volatile"]]
|
|
221
|
+
return projected
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
225
|
+
value: dict[str, Any] = {}
|
|
226
|
+
for key, item in pairs:
|
|
227
|
+
if key in value:
|
|
228
|
+
raise ArtifactIntegrityError(f"Duplicate JSON object key: {key}")
|
|
229
|
+
value[key] = item
|
|
230
|
+
return value
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _validate_package_keys(value: Any, path: Path, *, exempt: bool = False) -> None:
|
|
234
|
+
if isinstance(value, dict):
|
|
235
|
+
for key, item in value.items():
|
|
236
|
+
if not exempt and not _SNAKE_CASE.fullmatch(key):
|
|
237
|
+
raise ArtifactIntegrityError(f"Non-snake_case package field {key!r} in {path}")
|
|
238
|
+
_validate_package_keys(
|
|
239
|
+
item,
|
|
240
|
+
path,
|
|
241
|
+
exempt=exempt
|
|
242
|
+
or key in {"metadata", "extras", "emitted_schemas", "triangle_counts", "element_global_ids"},
|
|
243
|
+
)
|
|
244
|
+
elif isinstance(value, list):
|
|
245
|
+
for item in value:
|
|
246
|
+
_validate_package_keys(item, path, exempt=exempt)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _validate_drawing_annotation_document(value: dict[str, Any], path: Path) -> None:
|
|
250
|
+
provider_id = value.get("provider_id")
|
|
251
|
+
annotations = value.get("annotations")
|
|
252
|
+
sheets = value.get("sheets")
|
|
253
|
+
if provider_id is not None and (not isinstance(provider_id, str) or _DRAWING_ID.fullmatch(provider_id) is None):
|
|
254
|
+
raise ArtifactIntegrityError(f"Invalid drawing annotation provider_id in {path}")
|
|
255
|
+
if not isinstance(annotations, list) or not isinstance(sheets, list):
|
|
256
|
+
raise ArtifactIntegrityError(f"Invalid drawing annotation arrays in {path}")
|
|
257
|
+
annotation_fields = {
|
|
258
|
+
"id",
|
|
259
|
+
"parent_id",
|
|
260
|
+
"sheet_id",
|
|
261
|
+
"primitive_type",
|
|
262
|
+
"role",
|
|
263
|
+
"content",
|
|
264
|
+
"child_ids",
|
|
265
|
+
"rendered_entity",
|
|
266
|
+
"renderer_presence",
|
|
267
|
+
}
|
|
268
|
+
ids: list[str] = []
|
|
269
|
+
for record in annotations:
|
|
270
|
+
if not isinstance(record, dict) or set(record) != annotation_fields:
|
|
271
|
+
raise ArtifactIntegrityError(f"Invalid drawing annotation record in {path}")
|
|
272
|
+
annotation_id = record.get("id")
|
|
273
|
+
presence = record.get("renderer_presence")
|
|
274
|
+
if (
|
|
275
|
+
not isinstance(annotation_id, str)
|
|
276
|
+
or _DRAWING_ID.fullmatch(annotation_id) is None
|
|
277
|
+
or not isinstance(record.get("sheet_id"), str)
|
|
278
|
+
or not isinstance(record.get("primitive_type"), str)
|
|
279
|
+
or not isinstance(record.get("role"), str)
|
|
280
|
+
or not isinstance(record.get("content"), dict)
|
|
281
|
+
or not isinstance(record.get("child_ids"), list)
|
|
282
|
+
or not all(isinstance(child, str) for child in record["child_ids"])
|
|
283
|
+
or type(record.get("rendered_entity")) is not bool
|
|
284
|
+
or presence != {"svg": True, "dxf": True, "pdf": True}
|
|
285
|
+
):
|
|
286
|
+
raise ArtifactIntegrityError(f"Invalid drawing annotation semantics in {path}")
|
|
287
|
+
parent_id = record.get("parent_id")
|
|
288
|
+
if parent_id is not None and (
|
|
289
|
+
not isinstance(parent_id, str) or _DRAWING_ID.fullmatch(parent_id) is None
|
|
290
|
+
):
|
|
291
|
+
raise ArtifactIntegrityError(f"Invalid drawing annotation parent in {path}")
|
|
292
|
+
ids.append(annotation_id)
|
|
293
|
+
if ids != sorted(ids) or len(ids) != len(set(ids)):
|
|
294
|
+
raise ArtifactIntegrityError(f"Drawing annotation IDs must be unique and sorted in {path}")
|
|
295
|
+
if provider_id is None and annotations:
|
|
296
|
+
raise ArtifactIntegrityError(f"Drawing annotations require a provider_id in {path}")
|
|
297
|
+
known = set(ids)
|
|
298
|
+
for record in annotations:
|
|
299
|
+
if record["parent_id"] is not None and record["parent_id"] not in known:
|
|
300
|
+
raise ArtifactIntegrityError(f"Unknown drawing annotation parent in {path}")
|
|
301
|
+
if any(child not in known for child in record["child_ids"]):
|
|
302
|
+
raise ArtifactIntegrityError(f"Unknown drawing annotation child in {path}")
|
|
303
|
+
sheet_fields = {
|
|
304
|
+
"sheet_id",
|
|
305
|
+
"view_id",
|
|
306
|
+
"page_number",
|
|
307
|
+
"viewport_bounds_mm",
|
|
308
|
+
"annotation_ids",
|
|
309
|
+
"pdf_text",
|
|
310
|
+
}
|
|
311
|
+
sheet_ids: list[str] = []
|
|
312
|
+
for page, sheet in enumerate(sheets, start=1):
|
|
313
|
+
if not isinstance(sheet, dict) or set(sheet) != sheet_fields:
|
|
314
|
+
raise ArtifactIntegrityError(f"Invalid drawing sheet record in {path}")
|
|
315
|
+
annotation_ids = sheet.get("annotation_ids")
|
|
316
|
+
viewport = sheet.get("viewport_bounds_mm")
|
|
317
|
+
pdf_text = sheet.get("pdf_text")
|
|
318
|
+
if (
|
|
319
|
+
not isinstance(sheet.get("sheet_id"), str)
|
|
320
|
+
or sheet.get("view_id") not in {"plan", "front", "side", "section"}
|
|
321
|
+
or sheet.get("page_number") != page
|
|
322
|
+
or not isinstance(viewport, list)
|
|
323
|
+
or len(viewport) != 4
|
|
324
|
+
or not all(isinstance(item, (int, float)) for item in viewport)
|
|
325
|
+
or viewport[0] >= viewport[2]
|
|
326
|
+
or viewport[1] >= viewport[3]
|
|
327
|
+
or not isinstance(annotation_ids, list)
|
|
328
|
+
or annotation_ids != sorted(annotation_ids)
|
|
329
|
+
or not all(isinstance(item, str) and item in known for item in annotation_ids)
|
|
330
|
+
or not isinstance(pdf_text, list)
|
|
331
|
+
or not all(isinstance(item, str) for item in pdf_text)
|
|
332
|
+
):
|
|
333
|
+
raise ArtifactIntegrityError(f"Invalid drawing sheet mapping in {path}")
|
|
334
|
+
sheet_ids.append(sheet["sheet_id"])
|
|
335
|
+
if len(sheet_ids) != len(set(sheet_ids)):
|
|
336
|
+
raise ArtifactIntegrityError(f"Duplicate drawing sheet record in {path}")
|
|
337
|
+
if any(record["sheet_id"] not in sheet_ids for record in annotations):
|
|
338
|
+
raise ArtifactIntegrityError(f"Drawing annotation references an unknown sheet in {path}")
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Strict parser for the versioned ``[tool.python-cad]`` project contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import tomllib
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal, cast
|
|
11
|
+
|
|
12
|
+
from ._project_state import authoritative_path
|
|
13
|
+
from .exceptions import ProjectConfigError, UnsafePathError
|
|
14
|
+
|
|
15
|
+
type FormatName = Literal["step", "ifc", "glb", "drawings", "quantities"]
|
|
16
|
+
FORMAT_ORDER: tuple[FormatName, ...] = ("step", "ifc", "glb", "drawings", "quantities")
|
|
17
|
+
_ALLOWED_KEYS = {
|
|
18
|
+
"schema-version",
|
|
19
|
+
"config",
|
|
20
|
+
"model",
|
|
21
|
+
"drawing-annotations",
|
|
22
|
+
"source-inputs",
|
|
23
|
+
"output-directory",
|
|
24
|
+
"default-formats",
|
|
25
|
+
}
|
|
26
|
+
_MODULE = re.compile(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\Z", re.ASCII)
|
|
27
|
+
_CALLABLE = re.compile(r"([A-Za-z_]\w*(?:\.[A-Za-z_]\w)*):([A-Za-z_]\w*)\Z", re.ASCII)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ProjectConfig:
|
|
32
|
+
schema_version: Literal[1]
|
|
33
|
+
config_target: str
|
|
34
|
+
model_target: str
|
|
35
|
+
drawing_annotations_target: str | None
|
|
36
|
+
source_inputs: tuple[str, ...]
|
|
37
|
+
output_directory: str
|
|
38
|
+
default_formats: tuple[FormatName, ...]
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def declared_sources(self) -> frozenset[str]:
|
|
42
|
+
return frozenset(self.source_inputs)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_project_root(project_root: Path | None = None) -> Path:
|
|
46
|
+
raw = project_root
|
|
47
|
+
if raw is None:
|
|
48
|
+
configured = os.environ.get("PYTHON_CAD_PROJECT_ROOT")
|
|
49
|
+
raw = Path(configured) if configured else Path.cwd()
|
|
50
|
+
root = raw.expanduser().resolve()
|
|
51
|
+
if not root.is_dir():
|
|
52
|
+
raise ProjectConfigError(f"Project root is not a directory: {root}")
|
|
53
|
+
return root
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_project_config(project_root: Path) -> ProjectConfig:
|
|
57
|
+
root = resolve_project_root(project_root)
|
|
58
|
+
pyproject = root / "pyproject.toml"
|
|
59
|
+
if pyproject.is_symlink() or not pyproject.is_file():
|
|
60
|
+
raise ProjectConfigError(f"Project configuration is missing or not a regular file: {pyproject}")
|
|
61
|
+
try:
|
|
62
|
+
document = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
63
|
+
except (OSError, UnicodeError, tomllib.TOMLDecodeError) as error:
|
|
64
|
+
raise ProjectConfigError(f"Unable to parse {pyproject}: {error}") from error
|
|
65
|
+
table = _python_cad_table(document)
|
|
66
|
+
unknown = sorted(set(table) - _ALLOWED_KEYS)
|
|
67
|
+
if unknown:
|
|
68
|
+
raise ProjectConfigError(f"Unknown [tool.python-cad] key(s): {', '.join(unknown)}")
|
|
69
|
+
|
|
70
|
+
schema_version = table.get("schema-version")
|
|
71
|
+
if type(schema_version) is not int or schema_version != 1:
|
|
72
|
+
raise ProjectConfigError(f"Unsupported [tool.python-cad] schema-version: {schema_version!r}; expected 1")
|
|
73
|
+
config_target = _module_target(table.get("config", "config"), "config")
|
|
74
|
+
model_target = _callable_target(table.get("model", "model:build_model"), "model")
|
|
75
|
+
drawing_value = table.get("drawing-annotations")
|
|
76
|
+
drawing_target = None if drawing_value is None else _callable_target(drawing_value, "drawing-annotations")
|
|
77
|
+
sources = _source_inputs(table.get("source-inputs"), root)
|
|
78
|
+
output = _output_directory(table.get("output-directory", "generated"), root)
|
|
79
|
+
formats = _formats(table.get("default-formats", list(FORMAT_ORDER)))
|
|
80
|
+
return ProjectConfig(
|
|
81
|
+
schema_version=1,
|
|
82
|
+
config_target=config_target,
|
|
83
|
+
model_target=model_target,
|
|
84
|
+
drawing_annotations_target=drawing_target,
|
|
85
|
+
source_inputs=sources,
|
|
86
|
+
output_directory=output,
|
|
87
|
+
default_formats=formats,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def split_callable_target(target: str) -> tuple[str, str]:
|
|
92
|
+
match = _CALLABLE.fullmatch(target)
|
|
93
|
+
if match is None:
|
|
94
|
+
raise ProjectConfigError(f"Invalid callable target: {target!r}")
|
|
95
|
+
return match.group(1), match.group(2)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def output_path(project_root: Path, config: ProjectConfig) -> Path:
|
|
99
|
+
return project_root.joinpath(*Path(config.output_directory).parts)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _python_cad_table(document: dict[str, Any]) -> dict[str, Any]:
|
|
103
|
+
tool = document.get("tool")
|
|
104
|
+
if not isinstance(tool, dict) or "python-cad" not in tool:
|
|
105
|
+
raise ProjectConfigError("Missing required [tool.python-cad] table")
|
|
106
|
+
table = tool["python-cad"]
|
|
107
|
+
if not isinstance(table, dict):
|
|
108
|
+
raise ProjectConfigError("[tool.python-cad] must be a TOML table")
|
|
109
|
+
return cast(dict[str, Any], table)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _module_target(value: Any, key: str) -> str:
|
|
113
|
+
if not isinstance(value, str) or _MODULE.fullmatch(value) is None:
|
|
114
|
+
raise ProjectConfigError(f"[tool.python-cad].{key} must be a dotted Python module name")
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _callable_target(value: Any, key: str) -> str:
|
|
119
|
+
if not isinstance(value, str) or _CALLABLE.fullmatch(value) is None:
|
|
120
|
+
raise ProjectConfigError(f"[tool.python-cad].{key} must have the form dotted.module:callable")
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _source_inputs(value: Any, root: Path) -> tuple[str, ...]:
|
|
125
|
+
if not isinstance(value, list) or not value or not all(isinstance(item, str) for item in value):
|
|
126
|
+
raise ProjectConfigError("[tool.python-cad].source-inputs must be a nonempty array of strings")
|
|
127
|
+
normalized: list[str] = []
|
|
128
|
+
for item in cast(list[str], value):
|
|
129
|
+
_, relative = authoritative_path(root, item)
|
|
130
|
+
normalized.append(relative)
|
|
131
|
+
if len(normalized) != len(set(normalized)):
|
|
132
|
+
raise ProjectConfigError("[tool.python-cad].source-inputs contains a duplicate path")
|
|
133
|
+
if "pyproject.toml" not in normalized:
|
|
134
|
+
raise ProjectConfigError("[tool.python-cad].source-inputs must include pyproject.toml")
|
|
135
|
+
return tuple(normalized)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _output_directory(value: Any, root: Path) -> str:
|
|
139
|
+
if (
|
|
140
|
+
not isinstance(value, str)
|
|
141
|
+
or not value
|
|
142
|
+
or "\\" in value
|
|
143
|
+
or "\x00" in value
|
|
144
|
+
or ":" in value
|
|
145
|
+
or any(part in {"", ".", ".."} for part in value.split("/"))
|
|
146
|
+
):
|
|
147
|
+
raise UnsafePathError("[tool.python-cad].output-directory must be a safe project-relative path")
|
|
148
|
+
path = Path(*value.split("/"))
|
|
149
|
+
if path.is_absolute():
|
|
150
|
+
raise UnsafePathError(f"Unsafe output-directory: {value!r}")
|
|
151
|
+
current = root
|
|
152
|
+
for part in path.parts:
|
|
153
|
+
current /= part
|
|
154
|
+
if current.is_symlink():
|
|
155
|
+
raise UnsafePathError(f"output-directory traverses a symlink: {value!r}")
|
|
156
|
+
if not current.resolve(strict=False).is_relative_to(root):
|
|
157
|
+
raise UnsafePathError(f"output-directory escapes the project root: {value!r}")
|
|
158
|
+
return path.as_posix()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _formats(value: Any) -> tuple[FormatName, ...]:
|
|
162
|
+
if not isinstance(value, list) or not value or not all(isinstance(item, str) for item in value):
|
|
163
|
+
raise ProjectConfigError("[tool.python-cad].default-formats must be a nonempty array of strings")
|
|
164
|
+
raw = cast(list[str], value)
|
|
165
|
+
if len(raw) != len(set(raw)):
|
|
166
|
+
raise ProjectConfigError("[tool.python-cad].default-formats contains a duplicate format")
|
|
167
|
+
unknown = sorted(set(raw) - set(FORMAT_ORDER))
|
|
168
|
+
if unknown:
|
|
169
|
+
raise ProjectConfigError(f"Unsupported format(s): {', '.join(unknown)}")
|
|
170
|
+
selected = set(raw)
|
|
171
|
+
return tuple(item for item in FORMAT_ORDER if item in selected)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Synchronize a transitional project environment with its legacy runtime lock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
PIN_PATTERN = re.compile(r"^([A-Za-z0-9_.-]+)==([^\s;]+)$")
|
|
14
|
+
LEGACY_OCP_DISTRIBUTIONS = ("cadquery-ocp", "cadquery-vtk")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def locked_versions(lock_path: Path) -> dict[str, str]:
|
|
18
|
+
pins: dict[str, str] = {}
|
|
19
|
+
for raw_line in lock_path.read_text(encoding="utf-8").splitlines():
|
|
20
|
+
line = raw_line.strip()
|
|
21
|
+
if not line or line.startswith("#"):
|
|
22
|
+
continue
|
|
23
|
+
match = PIN_PATTERN.fullmatch(line)
|
|
24
|
+
if not match:
|
|
25
|
+
raise RuntimeError(f"Runtime lock entry is not an exact package pin: {line}")
|
|
26
|
+
pins[match.group(1)] = match.group(2)
|
|
27
|
+
if not pins:
|
|
28
|
+
raise RuntimeError(f"Runtime lock contains no package pins: {lock_path}")
|
|
29
|
+
return pins
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def version_mismatches(
|
|
33
|
+
pins: dict[str, str],
|
|
34
|
+
*,
|
|
35
|
+
get_version: Callable[[str], str] | None = None,
|
|
36
|
+
) -> list[str]:
|
|
37
|
+
resolve_version = get_version or version
|
|
38
|
+
mismatches: list[str] = []
|
|
39
|
+
for name, expected in pins.items():
|
|
40
|
+
try:
|
|
41
|
+
installed = resolve_version(name)
|
|
42
|
+
except PackageNotFoundError:
|
|
43
|
+
mismatches.append(f"{name}: missing (requires {expected})")
|
|
44
|
+
else:
|
|
45
|
+
if installed != expected:
|
|
46
|
+
mismatches.append(f"{name}: {installed} (requires {expected})")
|
|
47
|
+
return mismatches
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def installed_legacy_ocp(
|
|
51
|
+
*,
|
|
52
|
+
get_version: Callable[[str], str] | None = None,
|
|
53
|
+
) -> list[str]:
|
|
54
|
+
resolve_version = get_version or version
|
|
55
|
+
installed: list[str] = []
|
|
56
|
+
for name in LEGACY_OCP_DISTRIBUTIONS:
|
|
57
|
+
try:
|
|
58
|
+
resolve_version(name)
|
|
59
|
+
except PackageNotFoundError:
|
|
60
|
+
continue
|
|
61
|
+
installed.append(name)
|
|
62
|
+
return installed
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def in_virtual_environment() -> bool:
|
|
66
|
+
return sys.prefix != sys.base_prefix
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def synchronize(lock_path: Path) -> bool:
|
|
70
|
+
"""Install the lock when pinned versions or package consistency have drifted."""
|
|
71
|
+
if not lock_path.is_file():
|
|
72
|
+
raise RuntimeError(f"Managed runtime lock is missing: {lock_path}")
|
|
73
|
+
pins = locked_versions(lock_path)
|
|
74
|
+
mismatches = version_mismatches(pins)
|
|
75
|
+
check = subprocess.run(
|
|
76
|
+
[sys.executable, "-m", "pip", "check"],
|
|
77
|
+
text=True,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
check=False,
|
|
80
|
+
)
|
|
81
|
+
if not mismatches and check.returncode == 0:
|
|
82
|
+
return False
|
|
83
|
+
if not in_virtual_environment():
|
|
84
|
+
details = "; ".join(mismatches) or (check.stdout + check.stderr).strip()
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
"Runtime dependencies need synchronization, but the selected Python is not "
|
|
87
|
+
f"inside a virtual environment ({details}). Create .venv and install {lock_path}."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
print("Synchronizing the project virtual environment with the managed runtime lock...")
|
|
91
|
+
legacy = installed_legacy_ocp() if "cadquery-ocp-novtk" in pins else []
|
|
92
|
+
if legacy:
|
|
93
|
+
subprocess.run(
|
|
94
|
+
[sys.executable, "-m", "pip", "uninstall", "-y", *legacy],
|
|
95
|
+
check=True,
|
|
96
|
+
)
|
|
97
|
+
subprocess.run(
|
|
98
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", "-r", str(lock_path)],
|
|
99
|
+
check=True,
|
|
100
|
+
)
|
|
101
|
+
subprocess.run([sys.executable, "-m", "pip", "check"], check=True)
|
|
102
|
+
remaining = version_mismatches(pins)
|
|
103
|
+
if remaining:
|
|
104
|
+
raise RuntimeError("Runtime synchronization did not satisfy the lock: " + "; ".join(remaining))
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
109
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
110
|
+
parser.add_argument("lock_path", type=Path, help="Managed requirements lock to enforce")
|
|
111
|
+
return parser.parse_args(argv)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: list[str] | None = None) -> int:
|
|
115
|
+
args = parse_args(argv)
|
|
116
|
+
changed = synchronize(args.lock_path.expanduser().resolve())
|
|
117
|
+
print("Runtime dependencies synchronized." if changed else "Runtime dependencies already match the managed lock.")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
try:
|
|
123
|
+
raise SystemExit(main())
|
|
124
|
+
except Exception as error:
|
|
125
|
+
print(f"Runtime dependency synchronization failed: {error}", file=sys.stderr)
|
|
126
|
+
raise SystemExit(1) from error
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"urn:python-cad-tools:schema:build-manifest:1","type":"object","required":["schema_id","schema_version","producer","project","source_inputs","source_set_hash","selected_formats","artifacts","design_semantic_hash","artifact_set_hash","stable_artifact_set_hash"],"properties":{"schema_id":{"const":"urn:python-cad-tools:schema:build-manifest:1"},"schema_version":{"const":1}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"urn:python-cad-tools:schema:design-manifest:1","type":"object","required":["schema_id","schema_version","model_id","model_name","artifact_stem","elements","semantic_hash"],"properties":{"schema_id":{"const":"urn:python-cad-tools:schema:design-manifest:1"},"schema_version":{"const":1}}}
|