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.
Files changed (236) hide show
  1. python_cad_tools/__init__.py +10 -0
  2. python_cad_tools/_locking.py +63 -0
  3. python_cad_tools/_project_loader.py +273 -0
  4. python_cad_tools/_project_state.py +65 -0
  5. python_cad_tools/_scaffold.py +66 -0
  6. python_cad_tools/_transaction.py +208 -0
  7. python_cad_tools/_viewer_static/app.webmanifest +18 -0
  8. python_cad_tools/_viewer_static/asset-manifest.json +1 -0
  9. python_cad_tools/_viewer_static/assets/animationGroup-Sd34ntWg.js +1 -0
  10. python_cad_tools/_viewer_static/assets/bakedVertexAnimation-Cd_pRdrn.js +316 -0
  11. python_cad_tools/_viewer_static/assets/basisTextureLoader-DWgvS_1G.js +1 -0
  12. python_cad_tools/_viewer_static/assets/brdfTextureTools-CWGiG1W2.js +2 -0
  13. python_cad_tools/_viewer_static/assets/bumpFragment-CWULheF1.js +127 -0
  14. python_cad_tools/_viewer_static/assets/bumpVertex-DqzFZayE.js +6 -0
  15. python_cad_tools/_viewer_static/assets/bumpVertexDeclaration-C5EFyTio.js +6 -0
  16. python_cad_tools/_viewer_static/assets/clipPlaneFragment-BXRtt4jj.js +46 -0
  17. python_cad_tools/_viewer_static/assets/clipPlaneVertex-B_VhD1oQ.js +37 -0
  18. python_cad_tools/_viewer_static/assets/cubemapToSphericalPolynomial-DRpaukza.js +1 -0
  19. python_cad_tools/_viewer_static/assets/dds-Dkb2Cx2N.js +1 -0
  20. python_cad_tools/_viewer_static/assets/dds.pure-DtvJsKvY.js +1 -0
  21. python_cad_tools/_viewer_static/assets/ddsTextureLoader-H78xux1g.js +1 -0
  22. python_cad_tools/_viewer_static/assets/default.fragment-oSQ-tWhe.js +469 -0
  23. python_cad_tools/_viewer_static/assets/default.vertex-DhFjw_Da.js +209 -0
  24. python_cad_tools/_viewer_static/assets/defaultUboDeclaration-BMDHNppx.js +5 -0
  25. python_cad_tools/_viewer_static/assets/dumpTools-BaNZPjUX.js +2 -0
  26. python_cad_tools/_viewer_static/assets/envTextureLoader-V0YnLcIM.js +1 -0
  27. python_cad_tools/_viewer_static/assets/exrTextureLoader-CjGyIK3x.js +1 -0
  28. python_cad_tools/_viewer_static/assets/flowGraphApplyForceBlock-CW5nuobR.js +1 -0
  29. python_cad_tools/_viewer_static/assets/flowGraphApplyImpulseBlock-LFqYIkhj.js +1 -0
  30. python_cad_tools/_viewer_static/assets/flowGraphArrayIndexBlock-BMOHUbBm.js +1 -0
  31. python_cad_tools/_viewer_static/assets/flowGraphBezierCurveEasingBlock-knmTiuEk.js +1 -0
  32. python_cad_tools/_viewer_static/assets/flowGraphBinaryOperationBlock-DFLuCAKJ.js +1 -0
  33. python_cad_tools/_viewer_static/assets/flowGraphBranchBlock-BJRHMXMo.js +1 -0
  34. python_cad_tools/_viewer_static/assets/flowGraphCachedOperationBlock-Dng-6jE0.js +1 -0
  35. python_cad_tools/_viewer_static/assets/flowGraphCancelDelayBlock-CgbK3CQZ.js +1 -0
  36. python_cad_tools/_viewer_static/assets/flowGraphCodeExecutionBlock-8zMtq9tg.js +1 -0
  37. python_cad_tools/_viewer_static/assets/flowGraphConditionalDataBlock-BiLr2rkI.js +1 -0
  38. python_cad_tools/_viewer_static/assets/flowGraphConsoleLogBlock-J1vcCtNP.js +1 -0
  39. python_cad_tools/_viewer_static/assets/flowGraphConstantBlock-CFb5nuaV.js +1 -0
  40. python_cad_tools/_viewer_static/assets/flowGraphContextBlock-DAeGVzhC.js +1 -0
  41. python_cad_tools/_viewer_static/assets/flowGraphCounterBlock-lZlAQiJd.js +1 -0
  42. python_cad_tools/_viewer_static/assets/flowGraphDataSwitchBlock-BIzvmbVy.js +1 -0
  43. python_cad_tools/_viewer_static/assets/flowGraphDebounceBlock-DvNuMVwy.js +1 -0
  44. python_cad_tools/_viewer_static/assets/flowGraphDebugBlock-Dymd2VEE.js +1 -0
  45. python_cad_tools/_viewer_static/assets/flowGraphDoNBlock-v68aoNLH.js +1 -0
  46. python_cad_tools/_viewer_static/assets/flowGraphEasingBlock-CTnGv92q.js +1 -0
  47. python_cad_tools/_viewer_static/assets/flowGraphFlipFlopBlock-DXsO0Chw.js +1 -0
  48. python_cad_tools/_viewer_static/assets/flowGraphForLoopBlock-CPpe9T75.js +1 -0
  49. python_cad_tools/_viewer_static/assets/flowGraphFunctionReferenceBlock-DjDev4Pq.js +1 -0
  50. python_cad_tools/_viewer_static/assets/flowGraphGLTFDataProvider-GIYCz4AQ.js +1 -0
  51. python_cad_tools/_viewer_static/assets/flowGraphGetAngularVelocityBlock-BStkOVeL.js +1 -0
  52. python_cad_tools/_viewer_static/assets/flowGraphGetAssetBlock-Eiqoivbz.js +1 -0
  53. python_cad_tools/_viewer_static/assets/flowGraphGetLinearVelocityBlock-B0cpV5ws.js +1 -0
  54. python_cad_tools/_viewer_static/assets/flowGraphGetPhysicsMassPropertiesBlock-B-H64yaI.js +1 -0
  55. python_cad_tools/_viewer_static/assets/flowGraphGetPropertyBlock-CUyOsPw0.js +1 -0
  56. python_cad_tools/_viewer_static/assets/flowGraphGetSoundVolumeBlock-C0FjrsUz.js +1 -0
  57. python_cad_tools/_viewer_static/assets/flowGraphGetVariableBlock-O4597zBl.js +1 -0
  58. python_cad_tools/_viewer_static/assets/flowGraphIndexOfBlock-E00Z3KXM.js +1 -0
  59. python_cad_tools/_viewer_static/assets/flowGraphInterpolationBlock-DnkA9MNH.js +1 -0
  60. python_cad_tools/_viewer_static/assets/flowGraphIsKeyPressedBlock-DDfeuAyf.js +1 -0
  61. python_cad_tools/_viewer_static/assets/flowGraphIsSoundPlayingBlock-DMXg8jdM.js +1 -0
  62. python_cad_tools/_viewer_static/assets/flowGraphJsonPointerParserBlock-OmCorK0y.js +1 -0
  63. python_cad_tools/_viewer_static/assets/flowGraphKeyDownEventBlock-RuVtj6g2.js +1 -0
  64. python_cad_tools/_viewer_static/assets/flowGraphKeyUpEventBlock-DO4pAfHi.js +1 -0
  65. python_cad_tools/_viewer_static/assets/flowGraphKeyboardEventBlock-Bx3pAzzh.js +1 -0
  66. python_cad_tools/_viewer_static/assets/flowGraphMathBlocks-7W1XYOC8.js +1 -0
  67. python_cad_tools/_viewer_static/assets/flowGraphMathCombineExtractBlocks-a4f0Kv6e.js +1 -0
  68. python_cad_tools/_viewer_static/assets/flowGraphMatrixMathBlocks-C5Z-y1Au.js +1 -0
  69. python_cad_tools/_viewer_static/assets/flowGraphMeshPickEventBlock-D4m-I4CS.js +1 -0
  70. python_cad_tools/_viewer_static/assets/flowGraphMultiGateBlock-BISVSMw6.js +1 -0
  71. python_cad_tools/_viewer_static/assets/flowGraphPauseAnimationBlock-BYP2Z-HG.js +1 -0
  72. python_cad_tools/_viewer_static/assets/flowGraphPauseSoundBlock-Drh1EeW6.js +1 -0
  73. python_cad_tools/_viewer_static/assets/flowGraphPhysicsCollisionEventBlock-BoziAQ75.js +1 -0
  74. python_cad_tools/_viewer_static/assets/flowGraphPlayAnimationBlock-Clry-68M.js +1 -0
  75. python_cad_tools/_viewer_static/assets/flowGraphPlaySoundBlock-LbBVxGBE.js +1 -0
  76. python_cad_tools/_viewer_static/assets/flowGraphPointerDownEventBlock-DIVXCSHh.js +1 -0
  77. python_cad_tools/_viewer_static/assets/flowGraphPointerMoveEventBlock-BNNOzY2W.js +1 -0
  78. python_cad_tools/_viewer_static/assets/flowGraphPointerOutEventBlock-B8f2dOFT.js +1 -0
  79. python_cad_tools/_viewer_static/assets/flowGraphPointerOverEventBlock-CQr8LCee.js +1 -0
  80. python_cad_tools/_viewer_static/assets/flowGraphPointerUpEventBlock-DMg_iEHl.js +1 -0
  81. python_cad_tools/_viewer_static/assets/flowGraphReceiveCustomEventBlock-B8GJm9Ai.js +1 -0
  82. python_cad_tools/_viewer_static/assets/flowGraphSceneReadyEventBlock-Dcdb29K2.js +1 -0
  83. python_cad_tools/_viewer_static/assets/flowGraphSceneTickEventBlock-BLE5p4mi.js +1 -0
  84. python_cad_tools/_viewer_static/assets/flowGraphSendCustomEventBlock-BMzDItQ5.js +1 -0
  85. python_cad_tools/_viewer_static/assets/flowGraphSequenceBlock-DRURovjn.js +1 -0
  86. python_cad_tools/_viewer_static/assets/flowGraphSetAngularVelocityBlock-Ba8dhkLM.js +1 -0
  87. python_cad_tools/_viewer_static/assets/flowGraphSetDelayBlock-CnQR--Gn.js +1 -0
  88. python_cad_tools/_viewer_static/assets/flowGraphSetLinearVelocityBlock-Dm1r3vkh.js +1 -0
  89. python_cad_tools/_viewer_static/assets/flowGraphSetPhysicsMotionTypeBlock-BNtMLimi.js +1 -0
  90. python_cad_tools/_viewer_static/assets/flowGraphSetPropertyBlock-C-uv2jhP.js +1 -0
  91. python_cad_tools/_viewer_static/assets/flowGraphSetSoundVolumeBlock-D9LDx3lg.js +1 -0
  92. python_cad_tools/_viewer_static/assets/flowGraphSetVariableBlock-DszzRB5X.js +1 -0
  93. python_cad_tools/_viewer_static/assets/flowGraphSoundEndedEventBlock-DeTDSudb.js +1 -0
  94. python_cad_tools/_viewer_static/assets/flowGraphStopAnimationBlock-DWuwoFWr.js +1 -0
  95. python_cad_tools/_viewer_static/assets/flowGraphStopSoundBlock-DEnj2cQw.js +1 -0
  96. python_cad_tools/_viewer_static/assets/flowGraphSwitchBlock-zs8Fp9ky.js +1 -0
  97. python_cad_tools/_viewer_static/assets/flowGraphThrottleBlock-BYAKUQXd.js +1 -0
  98. python_cad_tools/_viewer_static/assets/flowGraphTransformCoordinatesSystemBlock-DYP54m5P.js +1 -0
  99. python_cad_tools/_viewer_static/assets/flowGraphTypeToTypeBlocks-vFwn79bS.js +1 -0
  100. python_cad_tools/_viewer_static/assets/flowGraphUnaryOperationBlock-CaRh4Ef0.js +1 -0
  101. python_cad_tools/_viewer_static/assets/flowGraphVectorMathBlocks-BEIhMs-A.js +1 -0
  102. python_cad_tools/_viewer_static/assets/flowGraphWaitAllBlock-DFbELP_j.js +1 -0
  103. python_cad_tools/_viewer_static/assets/flowGraphWhileLoopBlock-C1OKQ-_F.js +1 -0
  104. python_cad_tools/_viewer_static/assets/fogFragment-DtDlQBnz.js +25 -0
  105. python_cad_tools/_viewer_static/assets/gaussianSplatting-Cfigjb15.js +242 -0
  106. python_cad_tools/_viewer_static/assets/gaussianSplatting-DIn8R8oF.js +296 -0
  107. python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-BxeRJ2k-.js +38 -0
  108. python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-DNDmvRs-.js +37 -0
  109. python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-D92z5jYn.js +86 -0
  110. python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-DSWfFnKQ.js +86 -0
  111. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-DBQE2gDk.js +12 -0
  112. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-qwx5ntqJ.js +17 -0
  113. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-CP2kd3jU.js +30 -0
  114. python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-Dr-fEg_W.js +19 -0
  115. python_cad_tools/_viewer_static/assets/geometry.fragment-CsJIm2H0.js +242 -0
  116. python_cad_tools/_viewer_static/assets/geometry.fragment-Dph3muZt.js +262 -0
  117. python_cad_tools/_viewer_static/assets/geometry.vertex-BXGMTA_J.js +212 -0
  118. python_cad_tools/_viewer_static/assets/geometry.vertex-Bm9Wg3Gi.js +214 -0
  119. python_cad_tools/_viewer_static/assets/glowBlurPostProcess.fragment-BJSwKIJD.js +7 -0
  120. python_cad_tools/_viewer_static/assets/glowMapGeneration.fragment-DcyS1cTF.js +63 -0
  121. python_cad_tools/_viewer_static/assets/glowMapGeneration.vertex-BGq1Fakj.js +75 -0
  122. python_cad_tools/_viewer_static/assets/glowMapMerge.fragment-DxcneU_F.js +25 -0
  123. python_cad_tools/_viewer_static/assets/glowMapMerge.vertex-DaJJKqMV.js +8 -0
  124. python_cad_tools/_viewer_static/assets/harmonicsFunctions-DzJcvNWG.js +16 -0
  125. python_cad_tools/_viewer_static/assets/hdrTextureLoader-BGQvgh0h.js +2 -0
  126. python_cad_tools/_viewer_static/assets/helperFunctions-Cugfp1Fz.js +87 -0
  127. python_cad_tools/_viewer_static/assets/iesTextureLoader-CO4G0p2M.js +2 -0
  128. python_cad_tools/_viewer_static/assets/index-B2zBEWsq.js +7007 -0
  129. python_cad_tools/_viewer_static/assets/index-DcTTrc-8.css +1 -0
  130. python_cad_tools/_viewer_static/assets/kernelBlur.fragment-B6CTXTxY.js +57 -0
  131. python_cad_tools/_viewer_static/assets/kernelBlur.fragment-BkKbudbW.js +57 -0
  132. python_cad_tools/_viewer_static/assets/kernelBlur.vertex-BYwFU5IS.js +11 -0
  133. python_cad_tools/_viewer_static/assets/kernelBlur.vertex-D371zUfT.js +11 -0
  134. python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-CgBGaCHg.js +1 -0
  135. python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-YS8JpqdO.js +1 -0
  136. python_cad_tools/_viewer_static/assets/ktxTextureLoader-ncMSJTjl.js +1 -0
  137. python_cad_tools/_viewer_static/assets/lightFragment-skHiFbKB.js +398 -0
  138. python_cad_tools/_viewer_static/assets/logDepthDeclaration-BR8XRz9F.js +4 -0
  139. python_cad_tools/_viewer_static/assets/logDepthVertex-UKrhEIvg.js +14 -0
  140. python_cad_tools/_viewer_static/assets/mainUVVaryingDeclaration-BxQWGFcG.js +4 -0
  141. python_cad_tools/_viewer_static/assets/mesh.vertexData.functions-ayNlGC5K.js +1 -0
  142. python_cad_tools/_viewer_static/assets/meshUboDeclaration-BnX046aQ.js +4 -0
  143. python_cad_tools/_viewer_static/assets/oitFragment-CTMCOfNP.js +605 -0
  144. python_cad_tools/_viewer_static/assets/openpbr.fragment-DPJPqi6p.js +1530 -0
  145. python_cad_tools/_viewer_static/assets/openpbr.fragment-Dv6qTVVi.js +1384 -0
  146. python_cad_tools/_viewer_static/assets/openpbr.vertex-Bn2wrdGL.js +364 -0
  147. python_cad_tools/_viewer_static/assets/openpbr.vertex-CHESW3gB.js +251 -0
  148. python_cad_tools/_viewer_static/assets/openpbrMaterial.pure-BdK7VgO_.js +3 -0
  149. python_cad_tools/_viewer_static/assets/openpbrMaterialLoadingAdapter-Bm_AHDUN.js +4 -0
  150. python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-BDa9qEoN.js +339 -0
  151. python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-Ba_ly4LE.js +343 -0
  152. python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-BY2lSH2C.js +5 -0
  153. python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-DajhikDS.js +6 -0
  154. python_cad_tools/_viewer_static/assets/packingFunctions-BHPwcGG2.js +4 -0
  155. python_cad_tools/_viewer_static/assets/packingFunctions-DTwzBRBx.js +4 -0
  156. python_cad_tools/_viewer_static/assets/pass.fragment-9oKU4pI3.js +4 -0
  157. python_cad_tools/_viewer_static/assets/passCube.fragment-CpAeiYx1.js +23 -0
  158. python_cad_tools/_viewer_static/assets/passCube.fragment-DHtSWVRF.js +23 -0
  159. python_cad_tools/_viewer_static/assets/pbr.fragment-cIhMKwLs.js +2560 -0
  160. python_cad_tools/_viewer_static/assets/pbr.vertex-BfdONsf2.js +255 -0
  161. python_cad_tools/_viewer_static/assets/pbrBRDFFunctions-B_ppOzPO.js +173 -0
  162. python_cad_tools/_viewer_static/assets/pbrDebug-DDdwL5_t.js +693 -0
  163. python_cad_tools/_viewer_static/assets/pbrIBLFunctions-b1zdpzOP.js +43 -0
  164. python_cad_tools/_viewer_static/assets/pbrMaterial.pure-BZA7yVhu.js +2 -0
  165. python_cad_tools/_viewer_static/assets/pbrMaterialLoadingAdapter-BRvKRnjl.js +1 -0
  166. python_cad_tools/_viewer_static/assets/pbrUboDeclaration-CMZp25k3.js +5 -0
  167. python_cad_tools/_viewer_static/assets/postprocess.vertex-DzdUpNbW.js +9 -0
  168. python_cad_tools/_viewer_static/assets/procedural.vertex-BnbuQ-1f.js +7 -0
  169. python_cad_tools/_viewer_static/assets/procedural.vertex-Dp-Ng7Pc.js +8 -0
  170. python_cad_tools/_viewer_static/assets/reflectionFunction-DBlr3oDs.js +83 -0
  171. python_cad_tools/_viewer_static/assets/rgbdDecode.fragment-CyvnZFOJ.js +5 -0
  172. python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-Dz4zLnoD.js +5 -0
  173. python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-iscgpA1_.js +5 -0
  174. python_cad_tools/_viewer_static/assets/sceneUboDeclaration-BzFIc4mb.js +11 -0
  175. python_cad_tools/_viewer_static/assets/textureProcessor.fragment-C9D0rd-M.js +151 -0
  176. python_cad_tools/_viewer_static/assets/textureProcessor.fragment-JMKyWZng.js +146 -0
  177. python_cad_tools/_viewer_static/assets/tgaTextureLoader-oxTElIQJ.js +1 -0
  178. python_cad_tools/_viewer_static/assets/vertexColorMixing-mdC9xZe1.js +418 -0
  179. python_cad_tools/_viewer_static/favicon.svg +10 -0
  180. python_cad_tools/_viewer_static/index.html +18 -0
  181. python_cad_tools/_viewer_static/sw.js +61 -0
  182. python_cad_tools/artifacts.py +546 -0
  183. python_cad_tools/build-info.json +1 -0
  184. python_cad_tools/build.py +270 -0
  185. python_cad_tools/cli.py +212 -0
  186. python_cad_tools/context.py +67 -0
  187. python_cad_tools/determinism.py +66 -0
  188. python_cad_tools/drawings/__init__.py +124 -0
  189. python_cad_tools/drawings/_catalog.py +79 -0
  190. python_cad_tools/drawings/_validation.py +238 -0
  191. python_cad_tools/elements.py +121 -0
  192. python_cad_tools/exceptions.py +45 -0
  193. python_cad_tools/exporters/__init__.py +8 -0
  194. python_cad_tools/exporters/base.py +24 -0
  195. python_cad_tools/exporters/drawings/__init__.py +5 -0
  196. python_cad_tools/exporters/drawings/annotations.py +444 -0
  197. python_cad_tools/exporters/drawings/dxf.py +110 -0
  198. python_cad_tools/exporters/drawings/model.py +70 -0
  199. python_cad_tools/exporters/drawings/pdf.py +119 -0
  200. python_cad_tools/exporters/drawings/primitives.py +93 -0
  201. python_cad_tools/exporters/drawings/projection.py +64 -0
  202. python_cad_tools/exporters/drawings/svg.py +135 -0
  203. python_cad_tools/exporters/glb.py +100 -0
  204. python_cad_tools/exporters/ifc.py +229 -0
  205. python_cad_tools/exporters/quantities.py +136 -0
  206. python_cad_tools/exporters/step.py +117 -0
  207. python_cad_tools/geometry.py +187 -0
  208. python_cad_tools/manifests.py +338 -0
  209. python_cad_tools/project_config.py +174 -0
  210. python_cad_tools/py.typed +1 -0
  211. python_cad_tools/runtime_sync.py +126 -0
  212. python_cad_tools/schemas/build-manifest-1.json +1 -0
  213. python_cad_tools/schemas/design-manifest-1.json +1 -0
  214. python_cad_tools/schemas/download-manifest-2.json +46 -0
  215. python_cad_tools/schemas/drawing-annotations-1.json +66 -0
  216. python_cad_tools/schemas/glb-manifest-1.json +1 -0
  217. python_cad_tools/schemas/ifc-validation-1.json +1 -0
  218. python_cad_tools/schemas/quantities-1.json +1 -0
  219. python_cad_tools/schemas/run-metadata-1.json +1 -0
  220. python_cad_tools/schemas/site-metadata-1.json +34 -0
  221. python_cad_tools/schemas/step-validation-1.json +1 -0
  222. python_cad_tools/site.py +676 -0
  223. python_cad_tools/templates/project/.gitignore +9 -0
  224. python_cad_tools/templates/project/README.md +38 -0
  225. python_cad_tools/templates/project/config.py +36 -0
  226. python_cad_tools/templates/project/drawing_annotations.py +117 -0
  227. python_cad_tools/templates/project/model.py +87 -0
  228. python_cad_tools/templates/project/pyproject.toml +27 -0
  229. python_cad_tools/units.py +207 -0
  230. python_cad_tools/validation.py +153 -0
  231. python_cad_tools-0.1.0rc1.dist-info/METADATA +149 -0
  232. python_cad_tools-0.1.0rc1.dist-info/RECORD +236 -0
  233. python_cad_tools-0.1.0rc1.dist-info/WHEEL +5 -0
  234. python_cad_tools-0.1.0rc1.dist-info/entry_points.txt +2 -0
  235. python_cad_tools-0.1.0rc1.dist-info/licenses/LICENSE +679 -0
  236. 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
+ ]
@@ -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")