""" 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()