I'd start with this (this is for v0.7 version).
model = OP2()
model.read_op2(op2_filename)
isubcase = 1
itime = 0
quad_stress = model.cquad4_stress[isubcase]
#[fiber_dist, oxx, oyy, txy, angle, majorP, minorP, ovm] -> 5 for max_principal
max_principal_stress = quad_stress[itime, :, 5]
nnodes = quad_stress.nnodes # this will be 1 or 5 (centroid or 4+centroid) for cquads
So, here's where you can get some differences. If you have bilinear elements, presumably you'll drop the centroidal stresses and just use the nodal stresses. In that case, you can just get the node ids from:
nodes = quad_stress.element_node[:, 1]
i = where(nodes != 0)[0]
nodes_only = nodes[i]
max_principal_nodes_only = max_principal_stress[i]
Since you'll have nodes with the same IDs on different elements, you'll now need to average the stresses.
If you have centroidal stresses, you'll need to map the element ids to the nodes ids, pull the stresses, and average the stresses. You'll need to read the BDF and loop over the elements to extract the node IDs in that case.
Once you have the node IDs, you can use:
disp = model.displacements[isubcase]
# tx, ty, tz, rx, ry, rz - be careful of the CD coordinate system
t_xyz = disp.data[itime, :, 3]
disp_nodes = disp.node_gridtype[:, 0]
Then use the unique set of nodes that you want displacements for:
unodes = unique(nodes_only)
and loop over them to get the index:
j = searchsorted(disp_nodes, unodes)
and get the displacements:
disp_out = t_xyz[j, :]
Then just do that for the different element types (e.g. ctria3_stress, ctria6_stress, etc.) and write them out.