This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
openstaad is a Python package that wraps the OpenSTAAD COM API of Bentley STAAD.Pro. It exists to hide the ctypes/COM boilerplate (SAFEARRAY marshalling, VARIANT by-ref out-parameters, type juggling) so users write plain Python.
Hard constraints that shape everything:
- Windows-only at runtime. It talks to a running STAAD.Pro instance over COM (
comtypes). It cannot connect — and most code cannot be exercised — without STAAD.Pro installed and a.STDmodel open. This dev machine is macOS, so runtime testing is not possible here; validate logic by reading, not by running. - No test framework and no CI tests.
test.pyandexamples/*.pyare manual smoke scripts that require a live STAAD.Pro session.
The package is a set of façade classes, one per OpenSTAAD sub-object, all in openstaad/ and re-exported from openstaad/__init__.py:
Root (top-level app), Geometry, Load, Output, Properties, Command, View, Design, Support.
Every class follows the same construction pattern (see openstaad/root.py, openstaad/geometry.py):
CoInitialize(), then connect:client.GetActiveObject("StaadPro.OpenSTAAD")for the active session, orclient.CoGetObject(filePath, dynamic=True)when afilePathis passed to the constructor. ACOMErroris re-raised asRuntimeError("Cannot connect to STAAD.Pro").- Grab the relevant COM sub-object (e.g.
root_com.Geometry) intoself._geometry/self._root/ etc. (Rootkeeps the top object itself.) - Iterate a hardcoded
self._functionslist of COM method names and call_FlagAsMethod(name)on each — comtypes requires this before a COM member can be invoked as a method. __getattr__delegates any unknown attribute to the underlying COM object, so unwrapped API calls still pass through (untyped, raw).
openstaad/tools.py holds the marshalling helpers shared by every module (from openstaad.tools import *). Two roles:
make_safe_array_*buildcomtypesSAFEARRAYs of a given size/type.make_variant_vt_ref/make_safe_bstrwrap them in aVARIANTwithVT_BYREFso the COM method can write into them (OpenSTAAD returns most data through out-parameters, not return values).
The idiom for wrapping an out-parameter method (this is the core thing to learn): allocate a safe array, wrap it in a by-ref variant, pass the variant to the COM call, then read the result back from variant[0]. Scalars use size-0/1 arrays with VT_I4/VT_R8; collections size the array from a prior count call and use VT_ARRAY | VT_I4. Examples: Geometry.GetNodeCoordinates (three double out-params) and Geometry.GetNodeList (array sized by GetNodeCount()). Length/coordinate results are commonly rounded to 3 decimals (round(v*1000)/1000).
When adding a new wrapped function: add its name to the class's self._functions list (so it gets _FlagAsMethod'd) and write the method using the out-parameter idiom above.
# Dev env (on Windows the venv activate script lives at venv/Scripts/activate)
python -m venv venv
pip install -r requirements.txt # comtypes==1.4.15
# Build the distribution (hatchling backend)
python -m build
# Manual smoke test — ONLY works on Windows with STAAD.Pro open
python test.pyPublishing to PyPI is driven entirely by pushing a git tag (.github/workflows/release.yaml), which builds and publishes via trusted publishing. Steps:
- Bump
versioninpyproject.toml(this is manual and separate — the tag does not set it). git tag 0.0.xthengit push origin --tags.
Tag format must match the workflow's regex: X.Y.Z optionally with an aN / bN / rcN suffix (e.g. 0.0.13, 0.0.13a2).
Gotcha:
release.yamlstill has placeholderenvvalues (PACKAGE_NAME: "<PACKAGE_NAME>"etc.). Thecheck_pypijob readsenv.PACKAGE_NAME, so that job queries the wrong PyPI URL until the placeholders are filled in.