Source code for aiida.cmdline.commands.devel

# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved.                     #
# This file is part of the AiiDA code.                                    #
#                                                                         #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file        #
# For further information please visit http://www.aiida.net               #
###########################################################################
import sys
import os

import aiida
from aiida.cmdline.baseclass import VerdiCommandWithSubcommands
from aiida.backends.utils import load_dbenv, is_dbenv_loaded
from aiida.cmdline import pass_to_django_manage, execname
from aiida.common.exceptions import InternalError, TestsNotAllowedError



[docs]def applyfunct_len(value): """ Return the length of an object. """ try: return len(value) except Exception as e: raise InternalError(e, 'Error in function len, probably the object has no "len"?')
[docs]def applyfunct_keys(value): """ Return the keys of a dictionary. """ try: return value.keys() except Exception as e: raise InternalError(e, 'Error in function keys, probably not a dict?')
[docs]def apply_function(function, value): """ The function must be defined in this file and be in the format applyfunct_FUNCNAME where FUNCNAME is the string passed as the parameter 'function'; applyfunct_FUNCNAME will accept only one parameter ('value') and return an appropriate value. """ function_prefix = "applyfunct_" if function is None: return value else: try: return globals()[function_prefix + function](value) except KeyError as e: # Raising an InternalError means that a default value is printed # if no function exists. # Instead, raising a ValueError will always get printed even if # a default value is printed, that is what one wants # raise InternalError( # real_exception=e, message = raise ValueError( "o such function %s. Available functions are: %s." % (function, ", ".join(i[len(function_prefix):] for i in globals() if i.startswith(function_prefix))))
[docs]class Devel(VerdiCommandWithSubcommands): """ AiiDA commands for developers Provides a set of tools for developers. For instance, it allows to run the django tests for the db application and the unittests of the AiiDA modules. If you want to limit the tests to a specific subset of modules, pass them as parameters. An invalid parameter will make the code print the list of all valid parameters. Note: the test called 'db' will run all db.* tests. """ base_allowed_test_folders = [ 'aiida.scheduler', 'aiida.transport', 'aiida.common', 'aiida.tests.work', 'aiida.utils', 'aiida.control', 'aiida.cmdline.tests' ] _dbrawprefix = "db" _dbprefix = _dbrawprefix + "."
[docs] def __init__(self, *args, **kwargs): from aiida.backends.tests import get_db_test_names from aiida.backends import settings db_test_list = get_db_test_names() super(Devel, self).__init__(*args, **kwargs) self.valid_subcommands = { 'tests': (self.run_tests, self.complete_tests), 'query': (self.run_query, self.complete_none), # For the moment, no completion 'setproperty': (self.run_setproperty, self.complete_properties), 'getproperty': (self.run_getproperty, self.complete_properties), 'delproperty': (self.run_delproperty, self.complete_properties), 'describeproperties': (self.run_describeproperties, self.complete_none), 'listproperties': (self.run_listproperties, self.complete_none), 'listislands': (self.run_listislands, self.complete_none), 'play': (self.run_play, self.complete_none), 'getresults': (self.calculation_getresults, self.complete_none), 'tickd': (self.tick_daemon, self.complete_none) } # The content of the dict is: # None for a simple folder test # a list of strings for db tests, one for each test to run self.allowed_test_folders = {} for k in self.base_allowed_test_folders: self.allowed_test_folders[k] = None for dbtest in db_test_list: self.allowed_test_folders["{}{}".format(self._dbprefix, dbtest)] = [dbtest] self.allowed_test_folders[self._dbrawprefix] = db_test_list
[docs] def complete_properties(self, subargs_idx, subargs): """ I complete with subargs that were not used yet. """ from aiida.common.setup import _property_table if subargs_idx == 0: return " ".join(_property_table.keys()) else: return ""
[docs] def run_describeproperties(self, *args): """ List all valid properties that can be stored in the AiiDA config file. Only properties listed in the ``_property_table`` of ``aida.common.setup`` can be used. """ from aiida.common.setup import _property_table, _NoDefaultValue if args: print >> sys.stderr, ("No parameters allowed for {}".format( self.get_full_command_name())) sys.exit(1) for prop in sorted(_property_table.keys()): if _property_table[prop][4] is None: valid_vals_str = "" else: valid_vals_str = " Valid values: {}.".format(",".join( str(_) for _ in _property_table[prop][4])) if isinstance(_property_table[prop][3], _NoDefaultValue): def_val_string = "" else: def_val_string = " (default: {})".format( _property_table[prop][3]) print "* {} ({}): {}{}{}".format(prop, _property_table[prop][1], _property_table[prop][2], def_val_string, valid_vals_str)
[docs] def calculation_getresults(self, *args): """ Routine to get a list of results of a set of calculations, still under development. """ from aiida.common.exceptions import AiidaException load_dbenv() from aiida.orm import JobCalculation as OrmCalculation from aiida.orm.utils import load_node class InternalError(AiidaException): def __init__(self, real_exception, message): self.real_exception = real_exception self.message = message def get_suggestions(key, correct_keys): import difflib import string similar_kws = difflib.get_close_matches(key, correct_keys) if len(similar_kws) == 1: return "(Maybe you wanted to specify %s?)" % similar_kws[0] elif len(similar_kws) > 1: return "(Maybe you wanted to specify one of these: %s?)" % string.join(similar_kws, ', ') else: return "(No similar keywords found...)" # define a function to retrieve the data from the dictionary def key_finder(in_dict, the_keys): parent_dict = in_dict parent_dict_name = '<root level>' for new_key in the_keys: try: parent_dict = parent_dict[new_key] parent_dict_name = new_key except KeyError as e: raise InternalError(e, "Unable to find the key '%s' in '%s' %s" % ( new_key, parent_dict_name, get_suggestions(new_key, parent_dict.keys()))) except Exception as e: if e.__class__ is not InternalError: raise InternalError(e, "Error retrieving the key '%s' withing '%s', maybe '%s' is not a dict?" % ( the_keys[1], the_keys[0], the_keys[0])) else: raise return parent_dict def index_finder(data, indices, list_name): if not indices: return data parent_data = data parent_name = list_name for idx in indices: try: index = int(idx) parent_data = parent_data[index] parent_name += ":%s" % index except ValueError as e: raise InternalError(e, "%s is not a valid integer (in %s)." % (idx, parent_name)) except IndexError as e: raise InternalError(e, "Index %s is out of bounds, length of list %s is %s" % ( index, parent_name, len(parent_data))) except Exception as e: raise InternalError(e, "Invalid index! Maybe %s is not a list?" % parent_name) return parent_data if not args: print >> sys.stderr, "Pass some parameters." sys.exit(1) # I convert it to a list arguments = list(args) try: sep_idx = arguments.index('--') except ValueError: print >> sys.stderr, "Separate parameter keys from job-ids with a --!" sys.exit(1) keys_to_retrieve = arguments[:sep_idx] job_list_str = arguments[sep_idx + 1:] try: job_list = [int(i) for i in job_list_str] except ValueError: print >> sys.stderr, "All PKs after -- must be valid integers." sys.exit(1) sep = '\t' # Default separator: a tab character try: idx = keys_to_retrieve.index('-s') # First pop removes the -s parameter, second pop (now with the same # index) retrieves the asked separator. Raises IndexError if nothing # is provided after -s. _ = keys_to_retrieve.pop(idx) sep = keys_to_retrieve.pop(idx) except IndexError: print >> sys.stderr, "After -s you have to pass a valid separator!" sys.exit(1) except ValueError: # No -s found, use default separator pass print_header = True # Default: print the header try: keys_to_retrieve.remove('--no-header') # If here, the key was found: I remove it from the list and set the # the print_header flag print_header = False except ValueError: # No --no-header found: pass pass # check if there is at least one thing to do if not job_list: print >> sys.stderr, "Failed recognizing a calculation PK." sys.exit(1) if not keys_to_retrieve: print >> sys.stderr, "Failed recognizing a key to parse." sys.exit(1) # load the data if print_header: print "## Job list: %s" % " ".join(job_list_str) print "#" + sep.join(str(i) for i in keys_to_retrieve) for job in job_list: values_to_print = [] in_found = True out_found = True c = load_node(job, parent_class=OrmCalculation) try: i = c.inp.parameters.get_dict() except AttributeError: i = {} in_found = False try: o = c.out.output_parameters.get_dict() except AttributeError: out_found = False o = {} io = {'extras': c.get_extras(), 'attrs': c.get_attrs(), 'i': i, 'o': o, 'pk': job, 'label': c.label, 'desc': c.description, 'state': c.get_state(), 'sched_state': c.get_scheduler_state(), 'owner': c.dbnode.user.email} try: for datakey_full in keys_to_retrieve: split_for_def = datakey_full.split('=') datakey_raw = split_for_def[0] # Still can contain the function to apply def_val = "=".join(split_for_def[1:]) if len(split_for_def) > 1 else None split_for_func = datakey_raw.split('/') datakey = split_for_func[0] function_to_apply = "/".join(split_for_func[1:]) if len(split_for_func) > 1 else None key_values = [str(i) for i in datakey.split(':')[0].split('.')] indices = [int(i) for i in datakey.split(':')[1:]] # empty list for simple variables. try: value_key = key_finder(io, key_values) value = index_finder(value_key, indices, list_name=datakey.split(':')[0]) # Will do the correct thing if no function is supplied # and function is therefore None value = apply_function(function=function_to_apply, value=value) except InternalError as e: # For any internal error, set the value to the default if # provided, otherwise re-raise the exception if def_val is not None: value = def_val else: if not out_found: if in_found: e.message += " [NOTE: No output JSON could be found]" else: e.message += " [NOTE: No output JSON nor input JSON could be found]" raise e values_to_print.append(value) # Not needed: now we can ask explicitly for a job number using the flag # 'jobnum' # values_to_print.append("# %s" % str(job)) # print on screen print sep.join(str(i) for i in values_to_print) except InternalError as e: print >> sys.stderr, e.message except Exception as e: print >> sys.stderr, "# Error loading job # %s (%s): %s" % (job, type(e), e)
[docs] def tick_daemon(self, *args): """ Call all the functions that the daemon would call if running once and return. """ from aiida.daemon.tasks import manual_tick_all manual_tick_all()
[docs] def run_listproperties(self, *args): """ List all found global AiiDA properties. """ import argparse from aiida.common.setup import ( _property_table, exists_property, get_property) parser = argparse.ArgumentParser( prog=self.get_full_command_name(), description='List all custom properties stored in the user configuration file.') parser.add_argument('-a', '--all', dest='all', action='store_true', help="Show all properties, even if not explicitly defined, if they " "have a default value.") parser.set_defaults(all=False) parsed_args = parser.parse_args(args) show_all = parsed_args.all for prop in sorted(_property_table.keys()): try: # To enforce the generation of an exception, even if # there is a default value if show_all or exists_property(prop): val = get_property(prop) print "{} = {}".format(prop, val) except KeyError: pass
[docs] def run_listislands(self, *args): """ List all AiiDA nodes, that have no parents and children. """ load_dbenv() from django.db.models import Q from aiida.orm.node import Node from aiida.backends.utils import get_automatic_user q_object = Q(user=get_automatic_user()) q_object.add(Q(parents__isnull=True), Q.AND) q_object.add(Q(children__isnull=True), Q.AND) node_list = Node.query(q_object).distinct().order_by('ctime') print "ID\tclass" for node in node_list: print "{}\t{}".format(node.pk, node.__class__.__name__)
[docs] def run_getproperty(self, *args): """ Get a global AiiDA property from the config file in .aiida. """ from aiida.common.setup import get_property if len(args) != 1: print >> sys.stderr, ("usage: {} PROPERTYNAME".format( self.get_full_command_name())) sys.exit() try: value = get_property(args[0]) except Exception as e: print >> sys.stderr, ("{} while getting the " "property: {}".format(type(e).__name__, e.message)) sys.exit(1) print "{}".format(value)
[docs] def run_delproperty(self, *args): """ Delete a global AiiDA property from the config file in .aiida. """ from aiida.common.setup import del_property if len(args) != 1: print >> sys.stderr, ("usage: {} PROPERTYNAME".format( self.get_full_command_name())) sys.exit() try: del_property(args[0]) except KeyError: print >> sys.stderr, ("No such property '{}' in the config " "file.".format(args[0])) sys.exit(1) except Exception as e: print >> sys.stderr, ("{} while getting the " "property: {}".format(type(e).__name__, e.message)) sys.exit(1) print "Property '{}' successfully deleted.".format(args[0])
[docs] def run_setproperty(self, *args): """ Define a global AiiDA property in the config file in .aiida. Only properties in the _property_table of aiida.common.setup can be modified. """ from aiida.common.setup import set_property if len(args) != 2: print >> sys.stderr, ("usage: {} PROPERTYNAME PROPERTYVALUE".format( self.get_full_command_name())) sys.exit() try: set_property(args[0], args[1]) except Exception as e: print >> sys.stderr, ("{} while storing the " "property: {}".format(type(e).__name__, e.message)) sys.exit(1)
[docs] def run_tests(self, *args): import unittest from aiida.backends import settings from aiida.backends.testbase import run_aiida_db_tests from aiida.backends.testbase import check_if_tests_can_run from aiida import settings settings.TESTING_MODE = True # For final summary test_failures = [] test_errors = [] test_skipped = [] tot_num_tests = 0 db_test_list = [] test_folders = [] do_db = False if args: for arg in args: if arg in self.allowed_test_folders: dbtests = self.allowed_test_folders[arg] # Anything that has been added is a DB test if dbtests is not None: do_db = True for dbtest in dbtests: db_test_list.append(dbtest) else: test_folders.append(arg) else: print >> sys.stderr, ( "{} is not a valid test. " "Allowed test folders are:".format(arg)) print >> sys.stderr, "\n".join( ' * {}'.format(a) for a in sorted( self.allowed_test_folders.keys())) sys.exit(1) else: # Without arguments, run all tests do_db = True for k, v in self.allowed_test_folders.iteritems(): if v is None: # Non-db tests test_folders.append(k) else: # DB test for dbtest in v: db_test_list.append(dbtest) for test_folder in test_folders: print "v" * 75 print ">>> Tests for module {} <<<".format(test_folder.ljust(50)) print "^" * 75 testsuite = unittest.defaultTestLoader.discover( test_folder, top_level_dir=os.path.dirname(aiida.__file__)) test_runner = unittest.TextTestRunner() test_results = test_runner.run(testsuite) test_failures.extend(test_results.failures) test_errors.extend(test_results.errors) test_skipped.extend(test_results.skipped) tot_num_tests += test_results.testsRun if do_db: if not is_dbenv_loaded(): load_dbenv() # Even if each test would fail if we are not in a test profile, # it's still better to not even run them in the case the profile # is not a test one. try: check_if_tests_can_run() except TestsNotAllowedError as e: print >> sys.stderr, e.message sys.exit(1) # project_dir = os.path.join(os.path.dirname(aiida.__file__), '..') # testsuite = unittest.defaultTestLoader.discover('test', top_level_dir=project_dir) # test_runner = unittest.TextTestRunner() # test_runner.run(testsuite) print "v" * 75 print (">>> Tests for {} db application".format(settings.BACKEND)) print "^" * 75 db_results = run_aiida_db_tests(db_test_list) test_skipped.extend(db_results.skipped) test_failures.extend(db_results.failures) test_errors.extend(db_results.errors) tot_num_tests += db_results.testsRun print "Final summary of the run of tests:" print "* Tests skipped: {}".format(len(test_skipped)) if test_skipped: print " Reasons for skipping:" for reason in sorted(set([_[1] for _ in test_skipped])): print " - {}".format(reason) print "* Tests run: {}".format(tot_num_tests) # This count is wrong, sometimes a test can both error and fail # apparently, and you can get negative numbers... #print "* Tests OK: {}".format(tot_num_tests - len(test_errors) - len(test_failures)) print "* Tests failed: {}".format(len(test_failures)) print "* Tests errored: {}".format(len(test_errors)) # If there was any failure report it with the # right exit code if test_failures or test_errors: sys.exit(len(test_failures) + len(test_errors))
[docs] def complete_tests(self, subargs_idx, subargs): """ I complete with subargs that were not used yet. """ # I remove the one on which I am, so if I wrote all of it but # did not press space, it will get completed other_subargs = subargs[:subargs_idx] + subargs[subargs_idx + 1:] # I create a list of the tests that are not already written on the # command line remaining_tests = ( set(self.allowed_test_folders) - set(other_subargs)) return " ".join(sorted(remaining_tests))
[docs] def get_querydict_from_keyvalue(self, key, separator_filter, value): from aiida.orm import (Code, Data, Calculation, DataFactory, CalculationFactory) from aiida.common.exceptions import MissingPluginError import re item, sep, subproperty = key.partition('.') the_type, realvalue = re.match("^(\([^\)]+\))?(.*)$", value).groups() value = realvalue if the_type is None: # default: string the_type = "(t)" if the_type == '(t)': cast_value = value attr_field = "tval" elif the_type == '(i)': cast_value = int(value) attr_field = "ival" elif the_type == '(f)': cast_value = float(value) attr_field = "fval" else: raise ValueError("For the moment only (t), (i) and (f) fields accepted. Got '{}' instead".format(the_type)) do_recursive = False if separator_filter: sep_filter_string = "__{}".format(separator_filter) else: sep_filter_string = "" if item == 'i': # input keystring = "inputs" do_recursive = True elif item == 'o': # input keystring = "outputs" do_recursive = True elif item == 'p': # input keystring = "parents" do_recursive = True elif item == 'c': # input keystring = "children" do_recursive = True elif item == 'a': # input keystring = "dbattributes" if '*' in subproperty: if sep_filter_string: raise ValueError("Only = allowed if * patterns in the attributes are used") regex_key = re.escape(subproperty) # I look for non-separators, i.e. the * only covers # at a single level, and does not go deeper in levels regex_key = regex_key.replace(r'\*', r'[^\.]+') # Match the full string return ({keystring + "__key__regex": r'^{}$'.format(regex_key), keystring + "__{}".format(attr_field): cast_value}, [keystring + "__key", keystring + "__{}".format(attr_field)] ) else: # Standard query return ({keystring + "__key": subproperty, keystring + "__{}".format(attr_field) + sep_filter_string: cast_value}, [keystring + "__key", keystring + "__{}".format(attr_field)] ) elif item == 'e': # input keystring = "dbextras" if '*' in subproperty: import re if sep_filter_string: raise ValueError("Only = allowed if * patterns in the extras are used") regex_key = re.escape(subproperty) # I look for non-separators, i.e. the * only covers # at a single level, and does not go deeper in levels regex_key = regex_key.replace(r'\*', r'[^\.]+') return ({keystring + "__key__regex": r'^{}$'.format(regex_key), keystring + "__ival": value}, [keystring + "__key", keystring + "__ival"], ) else: # Standard query return ({keystring + "__key": subproperty, keystring + "__ival" + sep_filter_string: value}, [keystring + "__key", keystring + "__ival"] ) elif item == 't' or item == 'type': if sep_filter_string: raise ValueError("Only = allowed for type") if subproperty: raise ValueError("Cannot pass subproperties to type") if value == 'code': DataClass = Code elif value == 'node': return ({}, []) elif value == 'calc' or value == 'calculation': DataClass = Calculation elif value == 'data': DataClass = Data else: try: DataClass = DataFactory(value) except MissingPluginError: try: DataClass = CalculationFactory(value) except MissingPluginError: raise ValueError("Unknown node type '{}'".format(value)) # Startswith so to get also subclasses return ({"type__startswith": DataClass._query_type_string}, ["type"]) else: # this is a 'direct' attribute: just create the query return ({key + sep_filter_string: value}, # all, including the dots [key]) if do_recursive: if sep: tempdict, templist = self.get_querydict_from_keyvalue( subproperty, separator_filter, value) # In this case the filter is added by the recursive function return ({"{}__{}".format(keystring, k): v for k, v in tempdict.iteritems()}, ["{}__{}".format(keystring, k) for k in templist] + ["{}__pk".format(keystring), "{}__type".format(keystring), "{}__label".format(keystring)], ) else: return ({keystring + sep_filter_string: value}, [keystring] + ["{}__pk".format(keystring), "{}__type".format(keystring), "{}__label".format(keystring)], ) raise NotImplementedError("Should not be here...")
# To be put in the right order! For instance, # You want to put ~= before =, because this has # to be checked first # First element of each tuple: string used as separator # Second element of each tuple: django filter to apply separators = [ ("~=", "iexact"), (">=", "gte"), ("<=", "lte"), (">", "gt"), ("<", "lt"), ("=", ""), ] # TO ADD: startswith, istartswith, endswith, iendswith, ymdHMS, isnull, in, contains, # TO ADD: support for other types
[docs] def parse_arg(self, arg): # pieces = arg.split("=") # if len(pieces) != 2: # raise ValueError("Each option must be in the key=value format") # key = pieces[0] # value = pieces[1] #import re #sep_regex = "|".join([re.escape(k) for k in self.separators.keys()]) #regex = re.match(r'(.+)(' + sep_regex + r')(.+)',arg) # key, sep, value = regex.groups() for s, django_filter in self.separators: key, sep, value = arg.partition(s) if sep: filter_to_apply = django_filter break if not sep: raise ValueError("No separator provided!") querydict = self.get_querydict_from_keyvalue(key, filter_to_apply, value) return querydict
[docs] def run_query(self, *args): load_dbenv() from django.db.models import Q from aiida.backends.djsite.db.models import DbNode # django_query = Q() django_query = DbNode.objects.filter() try: # NOT SURE THIS IS THE RIGHT WAY OF MANAGING NEGATION FOR # ATTRIBUTES!! queries = [(self.parse_arg(arg[1:] if arg.startswith('!') else arg), arg.startswith('!')) for arg in args] except ValueError as e: print "ERROR in format!" print e.message sys.exit(1) all_values = ['pk'] print "*** DEBUG: QUERY ***" + "*" * 52 for (q, v), n in queries: print "* {}{}".format("[NEG]" if n else "", q) print "*" * 72 for (query, values), negate in queries: all_values += list(values) if negate: if len(query) == 1: # django_query = django_query & ~Q(**query) django_query = django_query.filter(~Q(**query)) elif len(query) == 2: raise NotImplementedError( "The current implementation does not work for negation of attributes. See comments in source code.") ## NOT THE RIGHT WAY TO MANAGE NEGATIONS!! SEE AT THE ## END OF THE FILE temp = [(k, v) for k, v in query.iteritems()] if temp[0][0].endswith("__key"): # django_query = django_query & ( # Q(~Q(**{temp[1][0]: temp[1][1]}), # **{temp[0][0]: temp[0][1]}) django_query = django_query.filter(~Q(**{temp[1][0]: temp[1][1]}), **{temp[0][0]: temp[0][1]}) elif temp[1][0].endswith("__key"): django_query = django_query.filter(~Q(**{temp[0][0]: temp[0][1]}), **{temp[1][0]: temp[1][1]}) else: raise NotImplementedError("Should not be here... no key search?") else: raise NotImplementedError("Should not be here...") else: django_query = django_query.filter(Q(**query)) res = django_query.distinct().order_by('pk') res = res.values_list(*all_values) print res.query print "{} matching nodes found ({} removing duplicates).".format(len(res), len(set([_[0] for _ in res]))) # for node in res: # print "* {}".format(str(node)) # print "* {} ({}){}".format( # node.pk, node.get_aiida_class().__class__.__name__, # " [{}]".format(node.label) if node.label else "") for node in res: print "* {}".format(node[0]) for p, v in zip(all_values[1:], node[1:]): print " `-> {} = {}".format(p, v)
[docs] def run_play(self, *args): """ Open a browser and play the Aida triumphal march by Giuseppe Verdi """ import webbrowser webbrowser.open_new('http://upload.wikimedia.org/wikipedia/commons/3/32/Triumphal_March_from_Aida.ogg')
# In [11]: attr_res = DbAttribute.objects.filter(Q(key='cell.atoms'), ~Q(ival__gt=7)) # #In [12]: dbres = DbNode.objects.filter(outputs__dbattributes__in=attr_res).distinct() # #In [13]: print dbres.query #SELECT DISTINCT "db_dbnode"."id", "db_dbnode"."uuid", "db_dbnode"."type", "db_dbnode"."label", "db_dbnode"."description", "db_dbnode"."ctime", "db_dbnode"."mtime", "db_dbnode"."user_id", "db_dbnode"."dbcomputer_id", "db_dbnode"."nodeversion", "db_dbnode"."lastsyncedversion" FROM "db_dbnode" INNER JOIN "db_dblink" ON ("db_dbnode"."id" = "db_dblink"."input_id") INNER JOIN "db_dbnode" T3 ON ("db_dblink"."output_id" = T3."id") INNER JOIN "db_dbattribute" ON (T3."id" = "db_dbattribute"."dbnode_id") WHERE "db_dbattribute"."id" IN (SELECT U0."id" FROM "db_dbattribute" U0 WHERE (U0."key" = cell.atoms AND NOT ((U0."ival" > 7 AND U0."ival" IS NOT NULL)))) ## OR, if doing #dbres = DbNode.objects.filter(dbattributes__in=attr_res).distinct() #In [18]: print dbres.query #SELECT DISTINCT "db_dbnode"."id", "db_dbnode"."uuid", "db_dbnode"."type", "db_dbnode"."label", "db_dbnode"."description", "db_dbnode"."ctime", "db_dbnode"."mtime", "db_dbnode"."user_id", "db_dbnode"."dbcomputer_id", "db_dbnode"."nodeversion", "db_dbnode"."lastsyncedversion" FROM "db_dbnode" INNER JOIN "db_dbattribute" ON ("db_dbnode"."id" = "db_dbattribute"."dbnode_id") WHERE "db_dbattribute"."id" IN (SELECT U0."id" FROM "db_dbattribute" U0 WHERE (U0."key" = cell.atoms AND NOT ((U0."ival" > 7 AND U0."ival" IS NOT NULL))))