Files
pdm/helpers/test_batch_api.py

185 lines
7.0 KiB
Python

"""
IEdmFile13::ChangeState3 via comtypes vtable direct call.
Confirmed from gen_py IEdmFile13_vtables_:
- IEdmFile13 IID : {DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}
- ChangeState3 oVft = 432 → vtable slot 54
- Slot layout:
0-2 IUnknown (handled by comtypes base)
3-6 IDispatch (handled by comtypes base)
7-53 47 methods from IEdmObject5 … IEdmFile12 (placeholders)
54 ChangeState3
55 GetThumbnail
"""
import ctypes
import getpass
import pythoncom
import win32com.client
import win32com.client.gencache as gencache
import comtypes
import comtypes.automation
from comtypes import COMMETHOD, GUID, HRESULT
from comtypes.automation import IDispatch as CT_IDispatch
VAULT_NAME = "Drilling_Test"
TEST_PATH = r"C:\PDM\Drilling_Test\DWS\PileDRIVER\825 PileDRIVER\MFG\800-TT-001.SLDPRT"
FOLDER_PATH = r"C:\PDM\Drilling_Test\DWS\PileDRIVER\825 PileDRIVER\MFG"
TRANSITION_ID = 268
TO_STATE_ID = 9
EdmObject_File = 1
IID_IEdmFile13 = "{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}"
# ---------------------------------------------------------------------------
# comtypes interface — 47 placeholders put ChangeState3 at slot 54 (offset 432)
# ---------------------------------------------------------------------------
_phs = [COMMETHOD([], HRESULT, f"_ph{i}") for i in range(47)]
VARIANT_p = ctypes.POINTER(comtypes.automation.VARIANT)
class IEdmFile13_CT(CT_IDispatch):
_iid_ = GUID(IID_IEdmFile13)
_idlflags_ = ["dual", "oleautomation"]
_methods_ = _phs + [
COMMETHOD(
[], HRESULT, "ChangeState3",
(["in"], VARIANT_p, "poStateIdOrName"),
(["in"], VARIANT_p, "poTransitionIdOrName"),
(["in"], ctypes.c_long, "lFolderID"),
(["in"], ctypes.c_wchar_p, "bsComment"),
(["in"], ctypes.c_long, "lParentWnd"),
(["in"], ctypes.c_long, "lEdmStateFlags"),
(["in"], ctypes.c_wchar_p, "bsPasswd"),
),
COMMETHOD(
[], HRESULT, "GetThumbnail",
(["out", "retval"], ctypes.POINTER(ctypes.c_void_p), "pBitmap"),
),
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_i4_variant(val: int) -> comtypes.automation.VARIANT:
"""Return a 16-byte VARIANT with vt=VT_I4 and the given integer value."""
v = comtypes.automation.VARIANT()
v.vt = 3 # VT_I4
# The union value starts at byte offset 8 inside VARIANT
ctypes.cast(ctypes.byref(v, 8), ctypes.POINTER(ctypes.c_int))[0] = val
return v
def raw_ptr_from_pycom(py_com_obj) -> int:
"""
Read the IUnknown*/IDispatch* stored inside a pythoncom COM wrapper.
CPython 64-bit layout: ob_refcnt(8) | ob_type(8) | punk(8) → offset 16.
"""
return ctypes.c_uint64.from_address(id(py_com_obj) + 16).value
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
username = input("PDM Username: ")
password = getpass.getpass("PDM Password: ") # reused for bsPasswd below
print("\n=== Connect ===")
vault = gencache.EnsureDispatch("ConisioLib.EdmVault")
vault.Login(username, password, VAULT_NAME)
print(f"Logged in to {VAULT_NAME}")
folder_obj = vault.GetFolderFromPath(FOLDER_PATH)
result = vault.GetFileFromPath(TEST_PATH, folder_obj)
raw = result[0] if isinstance(result, tuple) else result
file_id = raw.ID
fold_id = folder_obj.ID
print(f"File ID: {file_id} Folder ID: {fold_id}")
file_obj = vault.GetObject(EdmObject_File, file_id)
file13_w32 = win32com.client.CastTo(file_obj, "IEdmFile13")
# -----------------------------------------------------------------------
print("\n=== Extract raw IEdmFile13* from pythoncom ===")
# -----------------------------------------------------------------------
py_disp = file13_w32._oleobj_ # PyIDispatch wrapping IEdmFile13
raw_ptr = raw_ptr_from_pycom(py_disp)
print(f"IEdmFile13* = {raw_ptr:#x}")
# Sanity: read the vtable pointer (first 8 bytes of the COM object)
vtbl_ptr = ctypes.c_uint64.from_address(raw_ptr).value
print(f"Vtable ptr = {vtbl_ptr:#x}")
if not raw_ptr or not vtbl_ptr:
print("ERROR: Could not read a valid COM pointer — aborting.")
return
# -----------------------------------------------------------------------
print("\n=== QI to IEdmFile13_CT via comtypes ===")
# -----------------------------------------------------------------------
# Cast to IUnknown so comtypes can call QueryInterface properly
ct_unk = ctypes.cast(raw_ptr, ctypes.POINTER(comtypes.IUnknown))
# py_disp must stay alive while ct_unk is in use (ct_unk is a borrowed ref)
try:
file13_ct = ct_unk.QueryInterface(IEdmFile13_CT)
print(f"QI succeeded: {file13_ct}")
except Exception as e:
print(f"QI failed: {e}")
import traceback; traceback.print_exc()
return
# -----------------------------------------------------------------------
print("\n=== Build VARIANTs ===")
# -----------------------------------------------------------------------
v_state = make_i4_variant(TO_STATE_ID)
v_trans = make_i4_variant(TRANSITION_ID)
print(f"v_state vt={v_state.vt} val={TO_STATE_ID}")
print(f"v_trans vt={v_trans.vt} val={TRANSITION_ID}")
# -----------------------------------------------------------------------
print(f"\n=== ChangeState3(state={TO_STATE_ID}, trans={TRANSITION_ID}, folder={fold_id}) ===")
# -----------------------------------------------------------------------
try:
hr = file13_ct.ChangeState3(
ctypes.byref(v_state),
ctypes.byref(v_trans),
ctypes.c_long(fold_id),
"Batch transition test",
ctypes.c_long(0),
ctypes.c_long(0),
password, # PDM password (required by this transition)
)
print(f"ChangeState3 returned HRESULT {hr:#010x}")
except Exception as e:
print(f"ChangeState3 raised: {e}")
import traceback; traceback.print_exc()
# Keep py_disp alive
del py_disp
return
# keep py_disp alive until after the call
del py_disp
# -----------------------------------------------------------------------
print("\n=== Verify state ===")
# -----------------------------------------------------------------------
fresh = vault.GetObject(EdmObject_File, file_id)
try:
state = fresh.CurrentState
if callable(state):
state = state()
name = state.Name if hasattr(state, "Name") else str(state)
print(f"New state: {name}")
if name in ("Approved", "AA"):
print("\n*** SUCCESS! ***")
else:
print("State did not reach Approved.")
except Exception as e:
print(f"Could not read new state: {e}")
if __name__ == "__main__":
main()