This is a sort of response to EjbTernaryRelationshipExample and OrTernaryRelationshipExample.

    #!/usr/bin/env python2

    import sqlalchemy as sa;

    de = sa.create_engine("postgres:///3way")
    md = sa.BoundMetaData(de)
    #md.engine.echo = True

    # inspect the table metadata
    for t in ['suppliers', 'parts', 'projects',
              'r_projectsparts', 'r_suppliersparts', 'r_supplierspartsprojects']:
        globals()[t] = sa.Table(t, md, autoload=True)

    # ORM session
    sn = sa.create_session(bind_to=de)#, echo_uow=True)

    # Full ORM, although it's not needed for this problem
    class Supplier(object):
        def __init__(self, sname):
            self.supplier_name = sname # missing Ruby's Struct here
    class Project(object):
        def __init__(self, pname):
            self.project_name = pname
    class Part(object):
        def __init__(self, pcode):
            self.part_code = pcode
    class SuppliedPart(object):
        def __init__(self, supp, part):
            self.part = part
            self.supplier = supp
    class RequiredPart(object):
        def __init__(self, proj, part):
            self.part = part
            self.project = proj
    class PotentialPart(object):
        def __init__(self, reqp, supp):
            # assuming you want this caught before the db sees it
            if reqp.part != supp.part:
                raise Exception("Parts mismatch")
            self.reqp = reqp
            self.supp = supp
    class AcceptedPart(PotentialPart):
        def __init__(self, reqp, supp):
            PotentialPart.__init__(self, reqp, supp)

    sa.mapper(Part, parts)
    sa.mapper(Supplier, suppliers)
    sa.mapper(Project, projects)
    sa.mapper(SuppliedPart, r_suppliersparts, properties = {
        'part': sa.relation(Part, backref='splist', lazy=False),
        'supplier': sa.relation(Supplier, backref='splist', lazy=False)})
    sa.mapper(RequiredPart, r_projectsparts, properties = {
        'part': sa.relation(Part, backref='rplist', lazy=False),
        'project': sa.relation(Project, backref='rplist', lazy=False)})
    sa.mapper(AcceptedPart, r_supplierspartsprojects, properties = {
        'reqp': sa.relation(RequiredPart, backref=sa.backref('ap', uselist=False),
                            lazy=False), # 1:1, but may be None
        'supp': sa.relation(SuppliedPart, backref=sa.backref('ap', uselist=False),
                            lazy=False)})

    # this is more or less a client-side view
    big_join = sa.join(projects, r_projectsparts).join(parts).               join(r_suppliersparts).join(suppliers) # problem with col names
    named_join = sa.select([projects.c.project_name, parts.c.part_code,
                            suppliers.c.supplier_name], from_obj=[big_join]).                            alias('potparts')
    # the fields are only needed if you want traversals or insertions
    sa.mapper(PotentialPart, named_join, properties = {
        'reqp': sa.relation(RequiredPart, backref='pplist',
                            primaryjoin=sa.and_(named_join.c.part_code==
                                                r_projectsparts.c.part_code,
                                                named_join.c.project_name==
                                                r_projectsparts.c.project_name),
                            lazy=False),
        'supp': sa.relation(SuppliedPart, backref='pplist',
                            primaryjoin=sa.and_(named_join.c.part_code==
                                                r_suppliersparts.c.part_code,
                                                named_join.c.supplier_name==
                                                r_suppliersparts.c.supplier_name),
                            lazy=False)})

    # ORM ends here, insert some data now to show that it works as expected:
    # all these assignments can be avoided with metaclasses, but KISS for now
    # SA only checks shallow identity, so you'd need a weakref hash, yuck...
    hammer			= Part('hammer')
    nails	 		= Part('nails')
    wood			= Part('wood')
    game			= Part('game')
    spice_rack 		= Project('spice rack')
    nice_meal 		= Project('nice meal')
    acme 			= Supplier('ACME')
    forrest			= Supplier('Forrest')
    acme_hammer 	= SuppliedPart(acme, hammer)
    acme_nails 		= SuppliedPart(acme, nails)
    forrest_wood 	= SuppliedPart(forrest, wood)
    forrest_game	= SuppliedPart(forrest, game)
    sr_hammer 		= RequiredPart(spice_rack, hammer)
    sr_wood 		= RequiredPart(spice_rack, wood)
    sr_nails 		= RequiredPart(spice_rack, nails)
    nm_game			= RequiredPart(nice_meal, game)
    sn.save(AcceptedPart(sr_hammer, acme_hammer))
    # ACME nails are substandard, so they're not accepted
    ## sn.save(AcceptedPart(sr_nails, acme_nails)) 
    sn.save(AcceptedPart(sr_wood, forrest_wood))
    sn.save(AcceptedPart(nm_game, forrest_game))

    sn.flush() # commit

    def parts_that_cannot(pname):
        """
        List all parts that suppliers cannot provide for the project
        and that the project actually requires, and only for those
        providers that provide at least one part; also count how many
        parts they *can* supply, and the total number of parts needed
        for the project: (total, [(supplier, [count, missing]), ...])
        """
        proj = sn.query(Project).selectone_by(project_name=pname)
        requires = frozenset((rp.part_code for rp in proj.rplist))
        hres = {}
        for part in sn.query(PotentialPart).select_by(project_name=pname):
            he = hres.setdefault(part.supplier_name, [0, []])
            he[0] = he[0] + 1
            he[1].append(part.part_code)
            hres[part.supplier_name] = he
        # do the set complement w/o SQL, probably more efficient
        # if most projects use only a small subset of the parts
        for hk, hv in hres.iteritems():
            hres[hk][1] = requires - frozenset(hv[1])
        res = hres.items()
        res.sort(key=lambda x: x[1][0], reverse=True)
        return (len(requires), res)

    def parts_that_cannot_meth(self):
        return parts_that_cannot(self.project_name)

    Project.suppliers_with_parts_they_cannot_provide = parts_that_cannot_meth

    print "---spice rack---", parts_that_cannot('spice rack')
    print "---nice meal---", nice_meal.suppliers_with_parts_they_cannot_provide() # OO way