diff --git a/aiida/orm/__init__.py b/aiida/orm/__init__.py index 5a2033f302..67a48877f7 100644 --- a/aiida/orm/__init__.py +++ b/aiida/orm/__init__.py @@ -18,6 +18,7 @@ from .comments import * from .computers import * from .entities import * +from .fields import * from .groups import * from .logs import * from .nodes import * @@ -78,6 +79,10 @@ 'OrmEntityLoader', 'ProcessNode', 'ProjectionData', + 'QbAttrField', + 'QbField', + 'QbFieldFilters', + 'QbFields', 'QueryBuilder', 'RemoteData', 'RemoteStashData', diff --git a/aiida/orm/authinfos.py b/aiida/orm/authinfos.py index d83718ba21..8bdff3dbad 100644 --- a/aiida/orm/authinfos.py +++ b/aiida/orm/authinfos.py @@ -16,6 +16,7 @@ from aiida.plugins import TransportFactory from . import entities, users +from .fields import QbField if TYPE_CHECKING: from aiida.orm import Computer, User @@ -43,11 +44,19 @@ def delete(self, pk: int) -> None: class AuthInfo(entities.Entity['BackendAuthInfo']): """ORM class that models the authorization information that allows a `User` to connect to a `Computer`.""" + __qb_fields__ = ( + QbField('enabled', dtype=bool, doc='Whether the instance is enabled'), + QbField('auth_params', dtype=Dict[str, Any], doc='Dictionary of authentication parameters'), + QbField('metadata', dtype=Dict[str, Any], doc='Dictionary of metadata'), + QbField('computer_pk', 'dbcomputer_id', dtype=int, doc='The PK of the computer'), + QbField('user_pk', 'aiidauser_id', dtype=int, doc='The PK of the user'), + ) + Collection = AuthInfoCollection @classproperty - def objects(cls: Type['AuthInfo']) -> AuthInfoCollection: # type: ignore[misc] # pylint: disable=no-self-argument - return AuthInfoCollection.get_cached(cls, get_manager().get_profile_storage()) + def objects(cls: Type['AuthInfo']) -> AuthInfoCollection: # type: ignore # pylint: disable=no-self-argument + return AuthInfoCollection.get_cached(cls, get_manager().get_profile_storage()) # type: ignore PROPERTY_WORKDIR = 'workdir' diff --git a/aiida/orm/comments.py b/aiida/orm/comments.py index a8e3a99ea2..5000debaca 100644 --- a/aiida/orm/comments.py +++ b/aiida/orm/comments.py @@ -15,6 +15,7 @@ from aiida.manage import get_manager from . import entities, users +from .fields import QbField if TYPE_CHECKING: from aiida.orm import Node, User @@ -66,11 +67,20 @@ def delete_many(self, filters: dict) -> List[int]: class Comment(entities.Entity['BackendComment']): """Base class to map a DbComment that represents a comment attached to a certain Node.""" + __qb_fields__ = ( + QbField('uuid', dtype=str, doc='The UUID of the comment'), + QbField('ctime', dtype=datetime, doc='Creation time of the comment'), + QbField('mtime', dtype=datetime, doc='Modified time of the comment'), + QbField('content', dtype=str, doc='Content of the comment'), + QbField('user_pk', 'user_id', dtype=int, doc='User PK that created the comment'), + QbField('node_pk', 'dbnode_id', dtype=int, doc='Node PK that the comment is attached to'), + ) + Collection = CommentCollection @classproperty - def objects(cls: Type['Comment']) -> CommentCollection: # type: ignore[misc] # pylint: disable=no-self-argument - return CommentCollection.get_cached(cls, get_manager().get_profile_storage()) + def objects(cls: Type['Comment']) -> CommentCollection: # type: ignore # pylint: disable=no-self-argument + return CommentCollection.get_cached(cls, get_manager().get_profile_storage()) # type: ignore def __init__(self, node: 'Node', user: 'User', content: Optional[str] = None, backend: Optional['Backend'] = None): """Create a Comment for a given node and user diff --git a/aiida/orm/computers.py b/aiida/orm/computers.py index 2a2361aaa8..63b9fc85c4 100644 --- a/aiida/orm/computers.py +++ b/aiida/orm/computers.py @@ -18,6 +18,7 @@ from aiida.plugins import SchedulerFactory, TransportFactory from . import entities, users +from .fields import QbField if TYPE_CHECKING: from aiida.orm import AuthInfo, User @@ -75,11 +76,21 @@ class Computer(entities.Entity['BackendComputer']): PROPERTY_WORKDIR = 'workdir' PROPERTY_SHEBANG = 'shebang' + __qb_fields__ = ( + QbField('uuid', dtype=str, doc='The UUID of the computer'), + QbField('label', dtype=str, doc='Label for the computer'), + QbField('description', dtype=str, doc='Description of the computer'), + QbField('hostname', dtype=str, doc='Hostname of the computer'), + QbField('transport_type', dtype=str, doc='Transport type of the computer'), + QbField('scheduler_type', dtype=str, doc='Scheduler type of the computer'), + QbField('metadata', dtype=Dict[str, Any], doc='Metadata of the computer'), + ) + Collection = ComputerCollection @classproperty - def objects(cls: Type['Computer']) -> ComputerCollection: # type: ignore[misc] # pylint: disable=no-self-argument - return ComputerCollection.get_cached(cls, get_manager().get_profile_storage()) + def objects(cls: Type['Computer']) -> ComputerCollection: # type: ignore # pylint: disable=no-self-argument + return ComputerCollection.get_cached(cls, get_manager().get_profile_storage()) # type: ignore def __init__( # pylint: disable=too-many-arguments self, diff --git a/aiida/orm/entities.py b/aiida/orm/entities.py index 62a1a35896..1d88c30fb6 100644 --- a/aiida/orm/entities.py +++ b/aiida/orm/entities.py @@ -12,7 +12,7 @@ import copy from enum import Enum from functools import lru_cache -from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Protocol, Type, TypeVar, cast +from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Protocol, Sequence, Type, TypeVar, cast from plumpy.base.utils import call_with_super_check, super_check @@ -20,6 +20,8 @@ from aiida.common.lang import classproperty, type_check from aiida.manage import get_manager +from .fields import EntityFieldMeta, QbField, QbFields + if TYPE_CHECKING: from aiida.orm.implementation import Backend, BackendEntity from aiida.orm.querybuilder import FilterType, OrderByType, QueryBuilder @@ -161,9 +163,13 @@ def count(self, filters: Optional['FilterType'] = None) -> int: return self.query(filters=filters).count() -class Entity(abc.ABC, Generic[BackendEntityType]): +class Entity(Generic[BackendEntityType], metaclass=EntityFieldMeta): """An AiiDA entity""" + fields: QbFields = QbFields() + + __qb_fields__: Sequence[QbField] = (QbField('pk', 'id', dtype=int, doc='The primary key of the entity'),) + @classproperty @abc.abstractmethod def objects(cls: EntityType) -> Collection[EntityType]: # pylint: disable=no-self-argument,disable=no-self-use diff --git a/aiida/orm/fields.py b/aiida/orm/fields.py new file mode 100644 index 0000000000..0e56fb4dbc --- /dev/null +++ b/aiida/orm/fields.py @@ -0,0 +1,297 @@ +# -*- 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 # +########################################################################### +"""Module which provides decorators for AiiDA ORM entity -> DB field mappings.""" +from abc import ABCMeta +from copy import deepcopy +from pprint import pformat +from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Type, Union + +__all__ = ('QbAttrField', 'QbField', 'QbFields', 'QbFieldFilters') + + +class QbField: + """A field of an ORM entity, accessible via the ``QueryBuilder``""" + __slots__ = ('_key', '_qb_field', '_doc', '_dtype', '_subscriptable') + + def __init__( + self, + key: str, + qb_field: Optional[str] = None, + *, + dtype: Optional[Type[Any]] = None, + doc: str = '', + subscriptable: bool = False + ) -> None: + """Initialise a ORM entity field, accessible via the ``QueryBuilder`` + + :param key: The key of the field on the ORM entity + :param qb_field: The name of the field in the QueryBuilder, if not equal to ``key`` + :param dtype: The data type of the field. If None, the field is of variable type. + :param doc: A docstring for the field + :param subscriptable: If True, a new field can be created by ``field["subkey"]`` + """ + self._key = key + self._qb_field = qb_field if qb_field is not None else key + self._doc = doc + self._dtype = dtype + self._subscriptable = subscriptable + + @property + def key(self) -> str: + return self._key + + @property + def qb_field(self) -> str: + return self._qb_field + + @property + def doc(self) -> str: + return self._doc + + @property + def dtype(self) -> Optional[Type[Any]]: + return self._dtype + + @property + def subscriptable(self) -> bool: + return self._subscriptable + + def _repr_type(self, value: Any) -> str: + """Return a string representation of the type of the value""" + if value == type(None): + return 'None' + if isinstance(value, type): + # basic types like int, str, etc. + return value.__qualname__ + if hasattr(value, '__origin__') and value.__origin__ == Union: + return 'typing.Union[' + ','.join(self._repr_type(t) for t in value.__args__) + ']' + return str(value) + + def __repr__(self) -> str: + dtype = self._repr_type(self.dtype) if self.dtype else '' + return ( + f'{self.__class__.__name__}({self.key!r}' + + (f', {self._qb_field!r}' if self._qb_field != self.key else '') + + (f', dtype={dtype}' if self.dtype else '') + + (f', subscriptable={self.subscriptable!r}' if self.subscriptable else '') + ')' + ) + + def __str__(self) -> str: + type_str = '?' if self.dtype is None else ( + self.dtype.__name__ if isinstance(self.dtype, type) else str(self.dtype) + ) + type_str = type_str.replace('typing.', '') + return f"{self.__class__.__name__}({self.qb_field}{'.*' if self.subscriptable else ''}) -> {type_str}" + + def __getitem__(self, key: str) -> 'QbField': + """Return a new QbField with a nested key.""" + if not self.subscriptable: + raise IndexError('This field is not subscriptable') + return self.__class__(f'{self.key}.{key}', f'{self.qb_field}.{key}') + + def __hash__(self): + return hash((self.key, self.qb_field)) + + # methods for creating QueryBuilder filter objects + # these methods mirror the syntax within SQLAlchemy + + def __eq__(self, value): + return QbFieldFilters(((self, '==', value),)) + + def __ne__(self, value): + return QbFieldFilters(((self, '!=', value),)) + + def __lt__(self, value): + return QbFieldFilters(((self, '<', value),)) + + def __le__(self, value): + return QbFieldFilters(((self, '<=', value),)) + + def __gt__(self, value): + return QbFieldFilters(((self, '>', value),)) + + def __ge__(self, value): + return QbFieldFilters(((self, '>=', value),)) + + def like(self, value: str): + """Return a filter for only string values matching the wildcard string. + + - The percent sign (`%`) represents zero, one, or multiple characters + - The underscore sign (`_`) represents one, single character + """ + if not isinstance(value, str): + raise TypeError('like must be a string') + return QbFieldFilters(((self, 'like', value),)) + + def ilike(self, value: str): + """Return a filter for only string values matching the (case-insensitive) wildcard string. + + - The percent sign (`%`) represents zero, one, or multiple characters + - The underscore sign (`_`) represents one, single character + """ + if not isinstance(value, str): + raise TypeError('ilike must be a string') + return QbFieldFilters(((self, 'ilike', value),)) + + def in_(self, value: Iterable[Any]): + """Return a filter for only values in the list""" + try: + value = set(value) + except TypeError: + raise TypeError('in_ must be iterable') + return QbFieldFilters(((self, 'in', value),)) + + def not_in(self, value: Iterable[Any]): + """Return a filter for only values not in the list""" + try: + value = set(value) + except TypeError: + raise TypeError('in_ must be iterable') + return QbFieldFilters(((self, '!in', value),)) + + # JSONB only, we should only show these if the field is a JSONB field + + # def contains(self, value): + # """Return a filter for only values containing these items""" + # return QbFieldFilters(((self, 'contains', value),)) + + # def has_key(self, value): + # """Return a filter for only values with these keys""" + # return QbFieldFilters(((self, 'has_key', value),)) + + # def of_length(self, value: int): + # """Return a filter for only array values of this length.""" + # if not isinstance(value, int): + # raise TypeError('of_length must be an integer') + # return QbFieldFilters(((self, 'of_length', value),)) + + # def longer(self, value: int): + # """Return a filter for only array values longer than this length.""" + # if not isinstance(value, int): + # raise TypeError('longer must be an integer') + # return QbFieldFilters(((self, 'longer', value),)) + + # def shorter(self, value: int): + # """Return a filter for only array values shorter than this length.""" + # if not isinstance(value, int): + # raise TypeError('shorter must be an integer') + # return QbFieldFilters(((self, 'shorter', value),)) + + +class QbAttrField(QbField): + """An attribute field of an ORM entity, accessible via the ``QueryBuilder``""" + + @property + def qb_field(self) -> str: + return f'attributes.{self._qb_field}' + + +class QbFieldFilters: + """An representation of a list of fields and their comparators.""" + + __slots__ = ('filters',) + + def __init__(self, filters: Sequence[Tuple[QbField, str, Any]]): + self.filters = list(filters) + + def __repr__(self) -> str: + return f'QbFieldFilters({self.filters})' + + def as_dict(self) -> Dict[str, Any]: + """Return the filters as a dictionary, for use in the QueryBuilder.""" + output = {} + for field, comparator, value in self.filters: + if field in output: + output[field]['and'].append({comparator: value}) + else: + output[field] = {'and': [{comparator: value}]} + return output + + def __and__(self, other: 'QbFieldFilters') -> 'QbFieldFilters': + """Concatenate two QbFieldFilters objects: ``a & b``.""" + if not isinstance(other, QbFieldFilters): + raise TypeError(f'Cannot add QbFieldFilters and {type(other)}') + return QbFieldFilters(self.filters + other.filters) + + +class QbFields: + """A readonly class for mapping attributes to database fields of an AiiDA entity.""" + + __isabstractmethod__ = False + + def __init__(self, fields: Optional[Dict[str, QbField]] = None): + self._fields = fields or {} + + def __repr__(self) -> str: + return pformat({key: str(value) for key, value in self._fields.items()}) + + def __str__(self) -> str: + return str({key: str(value) for key, value in self._fields.items()}) + + def __getitem__(self, key: str) -> QbField: + """Return an QbField by key.""" + return self._fields[key] + + def __getattr__(self, key: str) -> QbField: + """Return an QbField by key.""" + try: + return self._fields[key] + except KeyError: + raise AttributeError(key) + + def __contains__(self, key: str) -> bool: + """Return if the field key exists""" + return key in self._fields + + def __len__(self) -> int: + """Return the number of fields""" + return len(self._fields) + + def __iter__(self): + """Iterate through the field keys""" + return iter(self._fields) + + def __dir__(self): + """Return keys for tab competion.""" + return list(self._fields) + ['_dict'] + + @property + def _dict(self): + """Return a copy of the internal mapping""" + return deepcopy(self._fields) + + +class EntityFieldMeta(ABCMeta): + """A metaclass for entity fields, which adds a `fields` class attribute.""" + + def __init__(cls, name, bases, classdict): + super().__init__(name, bases, classdict) + + # only allow an existing fields attribute if has been generated from a subclass + current_fields = getattr(cls, 'fields', None) + if current_fields is not None and not isinstance(current_fields, QbFields): + raise ValueError(f"class '{cls}' already has a `fields` attribute set") + + # Find all fields + fields = {} + # Note: inspect.getmembers causes loading of AiiDA to fail + for key, attr in ((key, attr) for subcls in reversed(cls.__mro__) for key, attr in subcls.__dict__.items()): + + # __qb_fields__ should be a list of QbField instances + if key == '__qb_fields__': + assert isinstance( + attr, Sequence + ), f"class '{cls}' has a '__qb_fields__' attribute, but it is not a sequence" + for field in attr: + if not isinstance(field, QbField): + raise ValueError(f"__qb_fields__ attribute of class '{cls}' must be list of QbField instances") + fields[field.key] = field + + cls.fields = QbFields({key: fields[key] for key in sorted(fields)}) diff --git a/aiida/orm/groups.py b/aiida/orm/groups.py index 33d52dfb87..eead4a5789 100644 --- a/aiida/orm/groups.py +++ b/aiida/orm/groups.py @@ -8,8 +8,7 @@ # For further information please visit http://www.aiida.net # ########################################################################### """AiiDA Group entites""" -from abc import ABCMeta -from typing import TYPE_CHECKING, ClassVar, Optional, Sequence, Tuple, Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Sequence, Tuple, Type, TypeVar, Union, cast import warnings from aiida.common import exceptions @@ -17,6 +16,7 @@ from aiida.manage import get_manager from . import convert, entities, users +from .fields import EntityFieldMeta, QbField if TYPE_CHECKING: from aiida.orm import Node, User @@ -48,19 +48,19 @@ def load_group_class(type_string: str) -> Type['Group']: return group_class -class GroupMeta(ABCMeta): +class GroupMeta(EntityFieldMeta): """Meta class for `aiida.orm.groups.Group` to automatically set the `type_string` attribute.""" def __new__(cls, name, bases, namespace, **kwargs): from aiida.plugins.entry_point import get_entry_point_from_class - newcls = ABCMeta.__new__(cls, name, bases, namespace, **kwargs) # pylint: disable=too-many-function-args + newcls = super().__new__(cls, name, bases, namespace, **kwargs) # pylint: disable=too-many-function-args mod = namespace['__module__'] entry_point_group, entry_point = get_entry_point_from_class(mod, name) if entry_point_group is None or entry_point_group != 'aiida.groups': - newcls._type_string = None # type: ignore[attr-defined] + newcls._type_string = None message = f'no registered entry point for `{mod}:{name}` so its instances will not be storable.' warnings.warn(message) # pylint: disable=no-member else: @@ -112,6 +112,16 @@ def delete(self, pk: int) -> None: class Group(entities.Entity['BackendGroup'], entities.EntityExtrasMixin, metaclass=GroupMeta): """An AiiDA ORM implementation of group of nodes.""" + __qb_fields__ = ( + QbField('uuid', dtype=str, doc='The UUID of the group'), + QbField('type_string', dtype=str, doc='The type of the group'), + QbField('label', dtype=str, doc='The group label'), + QbField('description', dtype=str, doc='The group description'), + QbField('time', dtype=str, doc='The time of the group creation'), + QbField('extras', dtype=Dict[str, Any], doc='The group extras'), + QbField('user_pk', 'user_id', dtype=int, doc='The PK for the creating user'), + ) + # added by metaclass _type_string: ClassVar[Optional[str]] diff --git a/aiida/orm/implementation/querybuilder.py b/aiida/orm/implementation/querybuilder.py index 567b0cfdde..e5fb5238d1 100644 --- a/aiida/orm/implementation/querybuilder.py +++ b/aiida/orm/implementation/querybuilder.py @@ -60,6 +60,8 @@ class QueryDictType(TypedDict): # mapping: tag -> [] -> field -> 'func' -> 'max' | 'min' | 'count' # 'cast' -> 'b' | 'd' | 'f' | 'i' | 'j' | 't' project: Dict[str, List[Dict[str, Dict[str, Any]]]] + # mapping: tag -> field -> return key for iterdict method + project_map: Dict[str, Dict[str, str]] # list of mappings: tag -> [] -> field -> 'order' -> 'asc' | 'desc' # 'cast' -> 'b' | 'd' | 'f' | 'i' | 'j' | 't' order_by: List[Dict[str, List[Dict[str, Dict[str, str]]]]] diff --git a/aiida/orm/implementation/sqlalchemy/querybuilder/main.py b/aiida/orm/implementation/sqlalchemy/querybuilder/main.py index a6dd7a4ede..46f8af5a5f 100644 --- a/aiida/orm/implementation/sqlalchemy/querybuilder/main.py +++ b/aiida/orm/implementation/sqlalchemy/querybuilder/main.py @@ -107,6 +107,7 @@ def __init__(self, backend): 'path': [], 'filters': {}, 'project': {}, + 'project_map': {}, 'order_by': [], 'offset': None, 'limit': None, @@ -229,7 +230,8 @@ def iterdict(self, data: QueryDictType, batch_size: Optional[int]) -> Iterable[D field_name = self.get_corresponding_property( self.get_table_name(self._get_tag_alias(tag)), attrkey, self.inner_to_outer_schema ) - yield_result[tag][field_name] = self.to_backend(row[project_index]) + key = self._data['project_map'].get(tag, {}).get(field_name, field_name) + yield_result[tag][key] = self.to_backend(row[project_index]) yield yield_result @contextmanager diff --git a/aiida/orm/logs.py b/aiida/orm/logs.py index 885ffb7bb1..978f0c26a9 100644 --- a/aiida/orm/logs.py +++ b/aiida/orm/logs.py @@ -17,6 +17,7 @@ from aiida.manage import get_manager from . import entities +from .fields import QbField if TYPE_CHECKING: from aiida.orm import Node @@ -128,11 +129,21 @@ class Log(entities.Entity['BackendLog']): An AiiDA Log entity. Corresponds to a logged message against a particular AiiDA node. """ + __qb_fields__ = ( + QbField('uuid', dtype=str, doc='The UUID of the node'), + QbField('loggername', dtype=str, doc='The name of the logger'), + QbField('levelname', dtype=str, doc='The name of the log level'), + QbField('message', dtype=str, doc='The message of the log'), + QbField('time', dtype=datetime, doc='The time at which the log was created'), + QbField('metadata', dtype=Dict[str, Any], doc='The metadata of the log'), + QbField('node_pk', 'dbnode_id', dtype=int, doc='The PK for the node'), + ) + Collection = LogCollection @classproperty - def objects(cls: Type['Log']) -> LogCollection: # type: ignore[misc] # pylint: disable=no-self-argument - return LogCollection.get_cached(cls, get_manager().get_profile_storage()) + def objects(cls: Type['Log']) -> LogCollection: # type: ignore # pylint: disable=no-self-argument + return LogCollection.get_cached(cls, get_manager().get_profile_storage()) # type: ignore def __init__( self, diff --git a/aiida/orm/nodes/data/array/bands.py b/aiida/orm/nodes/data/array/bands.py index dfb8d4b22f..d94eb48bf2 100644 --- a/aiida/orm/nodes/data/array/bands.py +++ b/aiida/orm/nodes/data/array/bands.py @@ -13,11 +13,13 @@ in a Brillouin zone, and how to operate on them. """ from string import Template +from typing import List, Optional import numpy from aiida.common.exceptions import ValidationError from aiida.common.utils import join_labels, prettify_labels +from aiida.orm.fields import QbAttrField from .kpoints import KpointsData @@ -220,6 +222,11 @@ class BandsData(KpointsData): Class to handle bands data """ + __qb_fields__ = ( + QbAttrField('array_labels', dtype=Optional[List[str]], doc='Labels associated with the band arrays'), + QbAttrField('units', dtype=str, doc='Units in which the data in bands were stored'), + ) + def set_kpointsdata(self, kpointsdata): """ Load the kpoints from a kpoint object. @@ -354,11 +361,10 @@ def array_labels(self): return self.get_attribute('array_labels', None) @property - def units(self): + def units(self) -> str: """ Units in which the data in bands were stored. A string """ - # return copy.deepcopy(self._pbc) return self.get_attribute('units') @units.setter diff --git a/aiida/orm/nodes/data/array/kpoints.py b/aiida/orm/nodes/data/array/kpoints.py index 71ecc8cdd7..ab0cbd6b9b 100644 --- a/aiida/orm/nodes/data/array/kpoints.py +++ b/aiida/orm/nodes/data/array/kpoints.py @@ -12,8 +12,12 @@ lists and meshes of k-points (i.e., points in the reciprocal space of a periodic crystal structure). """ +from typing import List + import numpy +from aiida.orm.fields import QbAttrField + from .array import ArrayData __all__ = ('KpointsData',) @@ -36,6 +40,17 @@ class KpointsData(ArrayData): set_cell_from_structure methods. """ + __qb_fields__ = ( + QbAttrField('labels', dtype=List[str], doc='Labels associated with the list of kpoints'), + QbAttrField('label_numbers', dtype=List[int], doc='Index of the labels in the list of kpoints'), + QbAttrField('mesh', dtype=List[int], doc='Mesh of kpoints'), + QbAttrField('offset', dtype=List[float], doc='Offset of kpoints'), + QbAttrField('cell', dtype=List[List[float]], doc='Unit cell of the crystal, in Angstroms'), + QbAttrField('pbc1', dtype=bool, doc='True if the first lattice vector is periodic'), + QbAttrField('pbc2', dtype=bool, doc='True if the second lattice vector is periodic'), + QbAttrField('pbc3', dtype=bool, doc='True if the third lattice vector is periodic'), + ) + def get_description(self): """ Returns a string with infos retrieved from kpoints node's properties. diff --git a/aiida/orm/nodes/data/array/trajectory.py b/aiida/orm/nodes/data/array/trajectory.py index 8193ab6529..b12ce1a326 100644 --- a/aiida/orm/nodes/data/array/trajectory.py +++ b/aiida/orm/nodes/data/array/trajectory.py @@ -10,8 +10,10 @@ """ AiiDA class to deal with crystal structure trajectories. """ - import collections.abc +from typing import List + +from aiida.orm.fields import QbAttrField from .array import ArrayData @@ -24,6 +26,12 @@ class TrajectoryData(ArrayData): possibly with velocities). """ + __qb_fields__ = ( + QbAttrField('units_positions', 'units|positions', dtype=str), + QbAttrField('units_times', 'units|times', dtype=str), + QbAttrField('symbols', dtype=List[str], doc='list of symbols'), + ) + def __init__(self, structurelist=None, **kwargs): super().__init__(**kwargs) if structurelist is not None: @@ -265,7 +273,7 @@ def get_cells(self): return None @property - def symbols(self): + def symbols(self) -> List[str]: """ Return the array of symbols, if it has already been set. diff --git a/aiida/orm/nodes/data/base.py b/aiida/orm/nodes/data/base.py index 176b1445d0..af6db798df 100644 --- a/aiida/orm/nodes/data/base.py +++ b/aiida/orm/nodes/data/base.py @@ -10,6 +10,8 @@ """`Data` sub class to be used as a base for data containers that represent base python data types.""" from functools import singledispatch +from aiida.orm.fields import QbAttrField + from .data import Data __all__ = ('BaseType', 'to_aiida_type') @@ -24,6 +26,8 @@ def to_aiida_type(value): class BaseType(Data): """`Data` sub class to be used as a base for data containers that represent base python data types.""" + __qb_fields__ = (QbAttrField('value', doc='The value of the data'),) + def __init__(self, value=None, **kwargs): try: getattr(self, '_type') diff --git a/aiida/orm/nodes/data/cif.py b/aiida/orm/nodes/data/cif.py index 35cf6db881..fec103f989 100644 --- a/aiida/orm/nodes/data/cif.py +++ b/aiida/orm/nodes/data/cif.py @@ -7,12 +7,13 @@ # For further information on the license, see the LICENSE.txt file # # For further information please visit http://www.aiida.net # ########################################################################### -# pylint: disable=invalid-name,too-many-locals,too-many-statements +# pylint: disable=invalid-name,too-many-locals,too-many-statements,unsubscriptable-object """Tools for handling Crystallographic Information Files (CIF)""" - import re +from typing import List from aiida.common.utils import Capturing +from aiida.orm.fields import QbAttrField from .singlefile import SinglefileData @@ -259,6 +260,12 @@ class CifData(SinglefileData): _values = None _ase = None + __qb_fields__ = ( + QbAttrField('formulae', dtype=List[str]), + QbAttrField('spacegroup_numbers', dtype=List[str]), + QbAttrField('md5', dtype=str), + ) + def __init__(self, ase=None, file=None, filename=None, values=None, scan_type=None, parse_policy=None, **kwargs): """Construct a new instance and set the contents to that of the file. diff --git a/aiida/orm/nodes/data/code.py b/aiida/orm/nodes/data/code.py index 4306ca5aaf..76a942aed4 100644 --- a/aiida/orm/nodes/data/code.py +++ b/aiida/orm/nodes/data/code.py @@ -9,9 +9,11 @@ ########################################################################### """Data plugin represeting an executable code to be wrapped and called through a `CalcJob` plugin.""" import os +from typing import Optional from aiida.common import exceptions from aiida.common.log import override_log_level +from aiida.orm.fields import QbAttrField from .data import Data @@ -34,9 +36,25 @@ class Code(Data): methods (e.g., the set_preexec_code() can be used to load specific modules required for the code to be run). """ - # pylint: disable=too-many-public-methods + __qb_fields__ = ( + QbAttrField( + 'prepend_text', + dtype=Optional[str], + doc='The code that will be put in the scheduler script before the execution of the code' + ), + QbAttrField( + 'append_text', + dtype=Optional[str], + doc='The code that will be put in the scheduler script after the execution of the code' + ), + QbAttrField('input_plugin', dtype=Optional[str], doc='The name of the input plugin to be used for this code'), + QbAttrField('local_executable', dtype=Optional[str], doc='Path to a local executable'), + QbAttrField('remote_exec_path', dtype=Optional[str], doc='Remote path to executable'), + QbAttrField('is_local', dtype=Optional[bool], doc='Whether the code is local or remote'), + ) + def __init__(self, remote_computer_exec=None, local_executable=None, input_plugin_name=None, files=None, **kwargs): super().__init__(**kwargs) @@ -327,7 +345,7 @@ def set_prepend_text(self, code): """ self.set_attribute('prepend_text', str(code)) - def get_prepend_text(self): + def get_prepend_text(self) -> str: """ Return the code that will be put in the scheduler script before the execution, or an empty string if no pre-exec code was defined. diff --git a/aiida/orm/nodes/data/data.py b/aiida/orm/nodes/data/data.py index 0c80acc188..01d8d21746 100644 --- a/aiida/orm/nodes/data/data.py +++ b/aiida/orm/nodes/data/data.py @@ -8,9 +8,12 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Module with `Node` sub class `Data` to be used as a base class for data structures.""" +from typing import Optional + from aiida.common import exceptions from aiida.common.lang import override from aiida.common.links import LinkType +from aiida.orm.fields import QbAttrField from ..node import Node @@ -42,6 +45,8 @@ class Data(Node): _storable = True _unstorable_message = 'storing for this node has been disabled' + __qb_fields__ = (QbAttrField('source', dtype=Optional[dict], subscriptable=True, doc='Source of the data'),) + def __init__(self, *args, source=None, **kwargs): """Construct a new instance, setting the ``source`` attribute if provided as a keyword argument.""" super().__init__(*args, **kwargs) @@ -75,7 +80,7 @@ def clone(self): return clone @property - def source(self): + def source(self) -> Optional[dict]: """ Gets the dictionary describing the source of Data object. Possible fields: diff --git a/aiida/orm/nodes/data/dict.py b/aiida/orm/nodes/data/dict.py index 03d3eb9d10..941245aa55 100644 --- a/aiida/orm/nodes/data/dict.py +++ b/aiida/orm/nodes/data/dict.py @@ -9,8 +9,11 @@ ########################################################################### """`Data` sub class to represent a dictionary.""" import copy +from typing import Any +from typing import Dict as DictType from aiida.common import exceptions +from aiida.orm.fields import QbField from .base import to_aiida_type from .data import Data @@ -46,6 +49,10 @@ class Dict(Data): Finally, all dictionary mutations will be forbidden once the node is stored. """ + __qb_fields__ = ( + QbField('dict', 'attributes', dtype=DictType[str, Any], subscriptable=True, doc='Source of the data'), + ) + def __init__(self, value=None, **kwargs): """Initialise a ``Dict`` node instance. diff --git a/aiida/orm/nodes/data/remote/base.py b/aiida/orm/nodes/data/remote/base.py index 4b2ac74268..503770a667 100644 --- a/aiida/orm/nodes/data/remote/base.py +++ b/aiida/orm/nodes/data/remote/base.py @@ -11,6 +11,7 @@ import os from aiida.orm import AuthInfo +from aiida.orm.fields import QbAttrField from ..data import Data @@ -25,13 +26,14 @@ class RemoteData(Data): """ KEY_EXTRA_CLEANED = 'cleaned' + __qb_fields__ = (QbAttrField('remote_path', dtype=str),) def __init__(self, remote_path=None, **kwargs): super().__init__(**kwargs) if remote_path is not None: self.set_remote_path(remote_path) - def get_remote_path(self): + def get_remote_path(self) -> str: return self.get_attribute('remote_path') def set_remote_path(self, val): diff --git a/aiida/orm/nodes/data/remote/stash/base.py b/aiida/orm/nodes/data/remote/stash/base.py index 1fe4e315c3..091cd5e51d 100644 --- a/aiida/orm/nodes/data/remote/stash/base.py +++ b/aiida/orm/nodes/data/remote/stash/base.py @@ -2,6 +2,7 @@ """Data plugin that models an archived folder on a remote computer.""" from aiida.common.datastructures import StashMode from aiida.common.lang import type_check +from aiida.orm.fields import QbAttrField from ...data import Data @@ -27,6 +28,8 @@ class RemoteStashData(Data): _storable = False + __qb_fields__ = (QbAttrField('stash_mode', dtype=str, doc='The mode with which the data was stashed'),) + def __init__(self, stash_mode: StashMode, **kwargs): """Construct a new instance diff --git a/aiida/orm/nodes/data/remote/stash/folder.py b/aiida/orm/nodes/data/remote/stash/folder.py index ebe097fd1f..ab4790c48a 100644 --- a/aiida/orm/nodes/data/remote/stash/folder.py +++ b/aiida/orm/nodes/data/remote/stash/folder.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """Data plugin that models a stashed folder on a remote computer.""" -import typing +from typing import List, Tuple, Union from aiida.common.datastructures import StashMode from aiida.common.lang import type_check +from aiida.orm.fields import QbAttrField from .base import RemoteStashData @@ -18,7 +19,12 @@ class RemoteStashFolderData(RemoteStashData): _storable = True - def __init__(self, stash_mode: StashMode, target_basepath: str, source_list: typing.List, **kwargs): + __qb_fields__ = ( + QbAttrField('target_basepath', dtype=str, doc='The the target basepath'), + QbAttrField('source_list', dtype=List[str], doc='The list of source files that were stashed'), + ) + + def __init__(self, stash_mode: StashMode, target_basepath: str, source_list: List, **kwargs): """Construct a new instance :param stash_mode: the stashing mode with which the data was stashed on the remote. @@ -50,7 +56,7 @@ def target_basepath(self, value: str): self.set_attribute('target_basepath', value) @property - def source_list(self) -> typing.Union[typing.List, typing.Tuple]: + def source_list(self) -> Union[List, Tuple]: """Return the list of source files that were stashed. :return: the list of source files. @@ -58,7 +64,7 @@ def source_list(self) -> typing.Union[typing.List, typing.Tuple]: return self.get_attribute('source_list') @source_list.setter - def source_list(self, value: typing.Union[typing.List, typing.Tuple]): + def source_list(self, value: Union[List, Tuple]): """Set the list of source files that were stashed. :param value: the list of source files. diff --git a/aiida/orm/nodes/data/structure.py b/aiida/orm/nodes/data/structure.py index b65b736ced..a06bd55b81 100644 --- a/aiida/orm/nodes/data/structure.py +++ b/aiida/orm/nodes/data/structure.py @@ -15,9 +15,11 @@ import copy import functools import itertools +from typing import List, Optional from aiida.common.constants import elements from aiida.common.exceptions import UnsupportedSpeciesError +from aiida.orm.fields import QbAttrField from .data import Data @@ -719,6 +721,15 @@ class StructureData(Data): _dimensionality_label = {0: '', 1: 'length', 2: 'surface', 3: 'volume'} _internal_kind_tags = None + __qb_fields__ = ( + QbAttrField('pbc1', dtype=bool, doc='Whether periodic in the a direction'), + QbAttrField('pbc2', dtype=bool, doc='Whether periodic in the b direction'), + QbAttrField('pbc3', dtype=bool, doc='Whether periodic in the c direction'), + QbAttrField('cell', dtype=List[List[float]], doc='The cell parameters'), + QbAttrField('kinds', dtype=Optional[List[dict]], doc='The kinds of atoms'), + QbAttrField('sites', dtype=Optional[List[dict]], doc='The atomic sites'), + ) + def __init__( self, cell=None, @@ -1573,7 +1584,7 @@ def get_kind_names(self): return [k.name for k in self.kinds] @property - def cell(self): + def cell(self) -> List[List[float]]: """ Returns the cell shape. diff --git a/aiida/orm/nodes/node.py b/aiida/orm/nodes/node.py index 4b416e1b05..11a13135a6 100644 --- a/aiida/orm/nodes/node.py +++ b/aiida/orm/nodes/node.py @@ -10,7 +10,7 @@ # pylint: disable=too-many-lines,too-many-arguments """Package for node ORM classes.""" import copy -import datetime +from datetime import datetime import importlib from logging import Logger from typing import ( @@ -43,6 +43,7 @@ from ..computers import Computer from ..entities import Collection as EntityCollection from ..entities import Entity, EntityAttributesMixin, EntityExtrasMixin +from ..fields import QbField from ..querybuilder import QueryBuilder from ..users import User from .repository import NodeRepositoryMixin @@ -149,6 +150,20 @@ class Node( # These are to be initialized in the `initialization` method _incoming_cache: Optional[List[LinkTriple]] = None + __qb_fields__: Sequence[QbField] = ( + QbField('uuid', dtype=str, doc='The UUID of the node'), + QbField('label', dtype=str, doc='The node label'), + QbField('description', dtype=str, doc='The node description'), + QbField('node_type', dtype=str, doc='The type of the node'), + QbField('ctime', dtype=datetime, doc='The creation time of the node'), + QbField('mtime', dtype=datetime, doc='The modification time of the node'), + QbField('repository_metadata', dtype=Dict[str, Any], doc='The repository virtual file system'), + QbField('extras', dtype=Dict[str, Any], subscriptable=True, doc='The node extras'), + QbField('user_pk', 'user_id', dtype=int, doc='The PK of the user who owns the node'), + # Subclasses denote specific keys in the attributes dict + # QbField('attributes', dtype=Dict[str, Any], subscriptable=True, doc='The node attributes'), + ) + Collection = NodeCollection @classproperty @@ -266,7 +281,6 @@ def uuid(self) -> str: """Return the node UUID. :return: the string representation of the UUID - """ return self.backend_entity.uuid @@ -381,7 +395,7 @@ def user(self, user: User) -> None: self.backend_entity.user = user.backend_entity # type: ignore[misc] @property - def ctime(self) -> datetime.datetime: + def ctime(self) -> datetime: """Return the node ctime. :return: the ctime @@ -389,7 +403,7 @@ def ctime(self) -> datetime.datetime: return self.backend_entity.ctime @property - def mtime(self) -> datetime.datetime: + def mtime(self) -> datetime: """Return the node mtime. :return: the mtime diff --git a/aiida/orm/nodes/process/calculation/calcjob.py b/aiida/orm/nodes/process/calculation/calcjob.py index bf4376e83d..25a21d6385 100644 --- a/aiida/orm/nodes/process/calculation/calcjob.py +++ b/aiida/orm/nodes/process/calculation/calcjob.py @@ -15,6 +15,7 @@ from aiida.common.datastructures import CalcJobState from aiida.common.lang import classproperty from aiida.common.links import LinkType +from aiida.orm.fields import QbAttrField from .calculation import CalculationNode @@ -36,8 +37,8 @@ class CalcJobNode(CalculationNode): # pylint: disable=too-many-public-methods - CALC_JOB_STATE_KEY = 'state' IMMIGRATED_KEY = 'imported' + CALC_JOB_STATE_KEY = 'state' REMOTE_WORKDIR_KEY = 'remote_workdir' RETRIEVE_LIST_KEY = 'retrieve_list' RETRIEVE_TEMPORARY_LIST_KEY = 'retrieve_temporary_list' @@ -47,6 +48,35 @@ class CalcJobNode(CalculationNode): SCHEDULER_LAST_JOB_INFO_KEY = 'last_job_info' SCHEDULER_DETAILED_JOB_INFO_KEY = 'detailed_job_info' + __qb_fields__ = ( + QbAttrField(SCHEDULER_STATE_KEY, dtype=Optional[str], doc='The state of the scheduler'), + QbAttrField(CALC_JOB_STATE_KEY, dtype=Optional[str], doc='The active state of the calculation job'), + QbAttrField(REMOTE_WORKDIR_KEY, dtype=Optional[str], doc='The path to the remote (on cluster) scratch folder'), + QbAttrField(SCHEDULER_JOB_ID_KEY, dtype=Optional[str], doc='The scheduler job id'), + QbAttrField( + SCHEDULER_LAST_CHECK_TIME_KEY, + dtype=Optional[str], + doc='The last time the scheduler was checked, in isoformat' + ), + QbAttrField( + SCHEDULER_LAST_JOB_INFO_KEY, dtype=Optional[str], doc='The last job info returned by the scheduler' + ), + QbAttrField( + SCHEDULER_DETAILED_JOB_INFO_KEY, + dtype=Optional[dict], + doc='The detailed job info returned by the scheduler' + ), + QbAttrField( + RETRIEVE_LIST_KEY, dtype=Optional[List[str]], doc='The list of files to retrieve from the remote cluster' + ), + QbAttrField( + RETRIEVE_TEMPORARY_LIST_KEY, + dtype=Optional[List[str]], + doc='The list of temporary files to retrieve from the remote cluster' + ), + QbAttrField(IMMIGRATED_KEY, dtype=Optional[bool], doc='Whether the node has been migrated'), + ) + # An optional entry point for a CalculationTools instance _tools = None @@ -159,7 +189,7 @@ def is_imported(self) -> bool: def get_option(self, name: str) -> Optional[Any]: """ - Retun the value of an option that was set for this CalcJobNode + Return the value of an option that was set for this CalcJobNode :param name: the option name :return: the option value or None diff --git a/aiida/orm/nodes/process/process.py b/aiida/orm/nodes/process/process.py index b02810a549..d1f2a5e750 100644 --- a/aiida/orm/nodes/process/process.py +++ b/aiida/orm/nodes/process/process.py @@ -10,12 +10,13 @@ """Module with `Node` sub class for processes.""" import enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Type, Union from plumpy.process_states import ProcessState from aiida.common.lang import classproperty from aiida.common.links import LinkType +from aiida.orm.fields import QbAttrField, QbField from aiida.orm.utils.mixins import Sealable from ..node import Node @@ -74,6 +75,18 @@ def _updatable_attributes(cls) -> Tuple[str, ...]: cls.PROCESS_STATUS_KEY, ) + __qb_fields__: Sequence[QbField] = ( + QbField('process_type', dtype=Optional[str], doc='The process type string'), + QbField('computer_pk', 'dbcomputer_id', dtype=Optional[int], doc='The computer PK'), + QbAttrField(PROCESS_LABEL_KEY, dtype=Optional[str], doc='The process label'), + QbAttrField(PROCESS_STATE_KEY, dtype=Optional[str], doc='The process state enum'), + QbAttrField(PROCESS_STATUS_KEY, dtype=Optional[str], doc='The process status is a generic status message'), + QbAttrField(EXIT_STATUS_KEY, dtype=Optional[int], doc='The process exit status'), + QbAttrField(EXIT_MESSAGE_KEY, dtype=Optional[str], doc='The process exit message'), + QbAttrField(EXCEPTION_KEY, dtype=Optional[str], doc='The process exception message'), + QbAttrField(PROCESS_PAUSED_KEY, dtype=bool, doc='Whether the process is paused'), + ) + @property def logger(self): """ diff --git a/aiida/orm/querybuilder.py b/aiida/orm/querybuilder.py index fcdf6a5fbd..c99e4760e2 100644 --- a/aiida/orm/querybuilder.py +++ b/aiida/orm/querybuilder.py @@ -48,7 +48,7 @@ QueryDictType, ) -from . import authinfos, comments, computers, convert, entities, groups, logs, nodes, users +from . import authinfos, comments, computers, convert, entities, fields, groups, logs, nodes, users if TYPE_CHECKING: # pylint: disable=ungrouped-imports @@ -60,7 +60,7 @@ # re-usable type annotations EntityClsType = Type[Union[entities.Entity, 'Process']] # pylint: disable=invalid-name ProjectType = Union[str, dict, Sequence[Union[str, dict]]] # pylint: disable=invalid-name -FilterType = Dict[str, Any] # pylint: disable=invalid-name +FilterType = Union[Dict[str, Any], fields.QbFieldFilters] # pylint: disable=invalid-name OrderByType = Union[dict, List[dict], Tuple[dict, ...]] @@ -104,6 +104,7 @@ def __init__( offset: Optional[int] = None, order_by: Optional[OrderByType] = None, distinct: bool = False, + project_map: Optional[Dict[str, Dict[str, str]]] = None, ) -> None: """ Instantiates a QueryBuilder instance. @@ -134,6 +135,7 @@ def __init__( How to order the results. As the 2 above, can be set also at later stage, check :func:`QueryBuilder.order_by` for more information. :param distinct: Whether to return de-duplicated rows + :param project_map: A mapping of the projection input-keys to the output-keys of `dict`/`iterdict` """ self._backend = backend or get_manager().get_profile_storage() @@ -146,6 +148,8 @@ def __init__( self._filters: Dict[str, FilterType] = {} # map tags to projections: tag -> list(fields) -> func | cast -> value self._projections: Dict[str, List[Dict[str, Dict[str, Any]]]] = {} + # mapping: tag -> field -> return key for iterdict/dict methods + self._project_map: Dict[str, Dict[str, str]] = {} # list of mappings: tag -> list(fields) -> 'order' | 'cast' -> value (str('asc' | 'desc'), str(cast_key)) self._order_by: List[Dict[str, List[Dict[str, Dict[str, str]]]]] = [] self._limit: Optional[int] = None @@ -170,6 +174,7 @@ def __init__( else: self.append(cls=path_spec) # Validate & add projections + self._init_project_map(project_map or {}) projection_dict = project or {} if not isinstance(projection_dict, dict): raise TypeError('You need to provide the projections as dictionary') @@ -200,6 +205,7 @@ def as_dict(self, copy: bool = True) -> QueryDictType: 'path': self._path, 'filters': self._filters, 'project': self._projections, + 'project_map': self._project_map, 'order_by': self._order_by, 'limit': self._limit, 'offset': self._offset, @@ -560,6 +566,23 @@ def append( return self + def _init_project_map(self, project_map: Dict[str, Dict[str, str]]) -> None: + """Set the project map. + + Note, this is a private method, + since the user should not override what is set by projected QbFields. + + :param dict project_map: The project map. + """ + if not isinstance(project_map, dict): + raise TypeError('project_map must be a dict') + for key, val in project_map.items(): + if not isinstance(key, str): + raise TypeError('project_map keys must be strings') + if not isinstance(val, dict): + raise TypeError('project_map values must be dicts') + self._project_map = project_map + def order_by(self, order_by: OrderByType) -> 'QueryBuilder': """ Set the entity to order by @@ -617,6 +640,8 @@ def order_by(self, order_by: OrderByType) -> 'QueryBuilder': for item_to_order_by in items_to_order_by: if isinstance(item_to_order_by, str): item_to_order_by = {item_to_order_by: {}} + elif isinstance(item_to_order_by, fields.QbField): + item_to_order_by = {item_to_order_by.qb_field: {}} elif isinstance(item_to_order_by, dict): pass else: @@ -686,7 +711,9 @@ def add_filter(self, tagspec: Union[str, EntityClsType], filter_spec: FilterType @staticmethod def _process_filters(filters: FilterType) -> Dict[str, Any]: """Process filters.""" - if not isinstance(filters, dict): + if isinstance(filters, fields.QbFieldFilters): + filters = filters.as_dict() + elif not isinstance(filters, dict): raise TypeError('Filters have to be passed as dictionaries') processed_filters = {} @@ -696,6 +723,8 @@ def _process_filters(filters: FilterType) -> Dict[str, Any]: # Convert to be the id of the joined entity because we can't query # for the object instance directly processed_filters[f'{key}_id'] = value.id + elif isinstance(key, fields.QbField): + processed_filters[key.qb_field] = value else: processed_filters[key] = value @@ -811,13 +840,28 @@ def add_projection(self, tag_spec: Union[str, EntityClsType], projection_spec: P tag = self._tags.get(tag_spec) _projections = [] self.debug('Adding projection of %s: %s', tag_spec, projection_spec) + + def _update_project_map(projection: fields.QbField): + """Return the DB field to use, or a tuple of the DB field to use and the key to return.""" + if projection.qb_field != projection.key: + self._project_map.setdefault(tag, {}) + self._project_map[tag][projection.qb_field] = projection.key + return projection.qb_field + if not isinstance(projection_spec, (list, tuple)): projection_spec = [projection_spec] # type: ignore for projection in projection_spec: if isinstance(projection, dict): - _thisprojection = projection + _thisprojection = { + _update_project_map(key) if isinstance(key, fields.QbField) else key: value + for key, value in projection.items() + } elif isinstance(projection, str): _thisprojection = {projection: {}} + elif isinstance(projection, fields.QbField): + _thisprojection = {_update_project_map(projection): {}} + elif isinstance(projection, fields.QbFields): + _thisprojection = {_update_project_map(projection[name]): {} for name in projection} else: raise ValueError(f'Cannot deal with projection specification {projection}\n') for spec in _thisprojection.values(): diff --git a/aiida/orm/users.py b/aiida/orm/users.py index abc56e2e19..f87f3c28d3 100644 --- a/aiida/orm/users.py +++ b/aiida/orm/users.py @@ -15,6 +15,7 @@ from aiida.manage import get_manager from . import entities +from .fields import QbField if TYPE_CHECKING: from aiida.orm.implementation import Backend, BackendUser @@ -70,11 +71,18 @@ def reset(self) -> None: class User(entities.Entity['BackendUser']): """AiiDA User""" + __qb_fields__ = ( + QbField('email', dtype=str, doc='The user email'), + QbField('first_name', dtype=str, doc='The user first name'), + QbField('last_name', dtype=str, doc='The user last name'), + QbField('institution', dtype=str, doc='The user institution'), + ) + Collection = UserCollection @classproperty - def objects(cls: Type['User']) -> UserCollection: # type: ignore[misc] # pylint: disable=no-self-argument - return UserCollection.get_cached(cls, get_manager().get_profile_storage()) + def objects(cls: Type['User']) -> UserCollection: # type: ignore # pylint: disable=no-self-argument + return UserCollection.get_cached(cls, get_manager().get_profile_storage()) # type: ignore def __init__( self, diff --git a/aiida/orm/utils/mixins.py b/aiida/orm/utils/mixins.py index 9857de86d7..1164181c88 100644 --- a/aiida/orm/utils/mixins.py +++ b/aiida/orm/utils/mixins.py @@ -14,6 +14,7 @@ from aiida.common import exceptions from aiida.common.lang import classproperty, override +from aiida.orm.fields import QbAttrField class FunctionCalculationMixin: @@ -119,6 +120,8 @@ class Sealable: SEALED_KEY = 'sealed' + __qb_fields__ = (QbAttrField(SEALED_KEY, dtype=bool, doc='Whether the node is sealed'),) + @classproperty def _updatable_attributes(cls): # pylint: disable=no-self-argument return (cls.SEALED_KEY,) diff --git a/aiida/orm/utils/node.py b/aiida/orm/utils/node.py index f6a26bb45b..ef634cf0dc 100644 --- a/aiida/orm/utils/node.py +++ b/aiida/orm/utils/node.py @@ -8,12 +8,12 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Utilities to operate on `Node` classes.""" -from abc import ABCMeta import logging import warnings from aiida.common import exceptions from aiida.common.utils import strip_prefix +from aiida.orm.fields import EntityFieldMeta __all__ = ( 'load_node_class', @@ -44,8 +44,8 @@ def load_node_class(type_string): try: base_path = type_string.rsplit('.', 2)[0] - except ValueError: - raise exceptions.EntryPointError from ValueError + except ValueError as exc: + raise exceptions.EntryPointError from exc # This exception needs to be there to make migrations work that rely on the old type string starting with `node.` # Since now the type strings no longer have that prefix, we simply strip it and continue with the normal logic. @@ -154,11 +154,11 @@ def get_query_type_from_type_string(type_string): return type_string -class AbstractNodeMeta(ABCMeta): +class AbstractNodeMeta(EntityFieldMeta): """Some python black magic to set correctly the logger also in subclasses.""" def __new__(cls, name, bases, namespace, **kwargs): - newcls = ABCMeta.__new__(cls, name, bases, namespace, **kwargs) # pylint: disable=too-many-function-args + newcls = super().__new__(cls, name, bases, namespace, **kwargs) # pylint: disable=too-many-function-args newcls._logger = logging.getLogger(f"{namespace['__module__']}.{name}") # Set the plugin type string and query type string based on the plugin type string diff --git a/tests/orm/test_fields.py b/tests/orm/test_fields.py new file mode 100644 index 0000000000..c394e76303 --- /dev/null +++ b/tests/orm/test_fields.py @@ -0,0 +1,138 @@ +# -*- 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 # +########################################################################### +# pylint: disable=missing-class-docstring,protected-access,unused-argument +"""Test for entity fields""" +from importlib_metadata import entry_points +import pytest + +from aiida import orm +from aiida.orm import Data, Dict, QueryBuilder, fields + +EPS = entry_points() + + +@pytest.mark.parametrize('entity_cls', (orm.AuthInfo, orm.Comment, orm.Computer, orm.Group, orm.Log, orm.User)) +def test_all_entity_fields(entity_cls, data_regression): + data_regression.check({key: repr(value) for key, value in entity_cls.fields._dict.items()}, + basename=f'fields_{entity_cls.__name__}') + + +@pytest.mark.parametrize( + 'group,name', ((group, name) for group in ('aiida.node', 'aiida.data') for name in EPS.select(group=group).names) +) +def test_all_node_fields(group, name, data_regression): + """Test that all the node fields are correctly registered.""" + node_cls = tuple(EPS.select(group=group, name=name))[0].load() + data_regression.check({key: repr(value) for key, value in node_cls.fields._dict.items()}, + basename=f'fields_{group}.{name}.{node_cls.__name__}') + + +def test_query_new_class(clear_database_before_test, monkeypatch): + """Test that fields are correctly registered on a new data class, + and can be used in a query. + """ + from aiida import plugins + + def _dummy(*args, **kwargs): + return True + + monkeypatch.setattr(plugins.entry_point, 'is_registered_entry_point', _dummy) + + class NewNode(Data): + __qb_fields__ = ( + fields.QbField('key1', 'attributes.key1'), + fields.QbAttrField('key2'), + fields.QbAttrField('key3'), + ) + + node = NewNode() + node.set_attribute_many({'key1': 2, 'key2': 2, 'key3': 3}) + node.store() + + node = NewNode() + node.set_attribute_many({'key1': 1, 'key2': 2, 'key3': 1}) + node.store() + + node = NewNode() + node.set_attribute_many({'key1': 4, 'key2': 5, 'key3': 6}) + node.store() + + result = QueryBuilder().append( + NewNode, + tag='node', + project=[NewNode.fields.key1, NewNode.fields.key2, NewNode.fields.key3], + filters={ + NewNode.fields.key2: 2 + } + ).order_by({ + 'node': NewNode.fields.ctime + }).all() + assert result == [[2, 2, 3], [1, 2, 1]] + + +def test_filter_operators(): + """Test that the operators are correctly registered.""" + field = Data.fields.pk + filters = (field == 1) & (field != 2) & (field > 3) & (field >= 4) & (field < 5) & (field <= 6) + # print(filters.as_dict()) + assert filters.as_dict() == { + fields.QbField('pk', qb_field='id', dtype=int): { + 'and': [{ + '==': 1 + }, { + '!=': 2 + }, { + '>': 3 + }, { + '>=': 4 + }, { + '<': 5 + }, { + '<=': 6 + }] + } + } + + +def test_filter_comparators(): + """Test that the comparators are correctly registered.""" + field = Data.fields.uuid + filters = (field.in_(['a'])) & (field.not_in(['b'])) & (field.like('a%')) & (field.ilike('a%')) + print(filters.as_dict()) + assert filters.as_dict() == { + fields.QbField('uuid', qb_field='uuid', dtype=str): { + 'and': [{ + 'in': {'a'} + }, { + '!in': {'b'} + }, { + 'like': 'a%' + }, { + 'ilike': 'a%' + }] + } + } + + +def test_query_filters(clear_database_before_test): + """Test using fields to generate a query filter.""" + node1 = Data().store() + Data().store() + filters = (Data.fields.pk == node1.pk) & (Data.fields.pk >= node1.pk) + result = QueryBuilder().append(Data, project=Data.fields.pk, filters=filters).all() + assert result == [[node1.pk]] + + +def test_query_subscriptable(clear_database_before_test): + """Test using subscriptable fields in a query.""" + node = Dict(dict={'a': 1}).store() + node.set_extra('b', 2) + result = QueryBuilder().append(Dict, project=[Dict.fields.dict['a'], Dict.fields.extras['b']]).all() + assert result == [[1, 2]] diff --git a/tests/orm/test_fields/fields_AuthInfo.yml b/tests/orm/test_fields/fields_AuthInfo.yml new file mode 100644 index 0000000000..f0f7eedb2f --- /dev/null +++ b/tests/orm/test_fields/fields_AuthInfo.yml @@ -0,0 +1,6 @@ +auth_params: QbField('auth_params', dtype=typing.Dict[str, typing.Any]) +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=int) +enabled: QbField('enabled', dtype=bool) +metadata: QbField('metadata', dtype=typing.Dict[str, typing.Any]) +pk: QbField('pk', 'id', dtype=int) +user_pk: QbField('user_pk', 'aiidauser_id', dtype=int) diff --git a/tests/orm/test_fields/fields_Comment.yml b/tests/orm/test_fields/fields_Comment.yml new file mode 100644 index 0000000000..3806a245a0 --- /dev/null +++ b/tests/orm/test_fields/fields_Comment.yml @@ -0,0 +1,7 @@ +content: QbField('content', dtype=str) +ctime: QbField('ctime', dtype=datetime) +mtime: QbField('mtime', dtype=datetime) +node_pk: QbField('node_pk', 'dbnode_id', dtype=int) +pk: QbField('pk', 'id', dtype=int) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_Computer.yml b/tests/orm/test_fields/fields_Computer.yml new file mode 100644 index 0000000000..72e6c4656a --- /dev/null +++ b/tests/orm/test_fields/fields_Computer.yml @@ -0,0 +1,8 @@ +description: QbField('description', dtype=str) +hostname: QbField('hostname', dtype=str) +label: QbField('label', dtype=str) +metadata: QbField('metadata', dtype=typing.Dict[str, typing.Any]) +pk: QbField('pk', 'id', dtype=int) +scheduler_type: QbField('scheduler_type', dtype=str) +transport_type: QbField('transport_type', dtype=str) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_Group.yml b/tests/orm/test_fields/fields_Group.yml new file mode 100644 index 0000000000..8cc298609c --- /dev/null +++ b/tests/orm/test_fields/fields_Group.yml @@ -0,0 +1,8 @@ +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any]) +label: QbField('label', dtype=str) +pk: QbField('pk', 'id', dtype=int) +time: QbField('time', dtype=str) +type_string: QbField('type_string', dtype=str) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_Log.yml b/tests/orm/test_fields/fields_Log.yml new file mode 100644 index 0000000000..e5c8ea81f8 --- /dev/null +++ b/tests/orm/test_fields/fields_Log.yml @@ -0,0 +1,8 @@ +levelname: QbField('levelname', dtype=str) +loggername: QbField('loggername', dtype=str) +message: QbField('message', dtype=str) +metadata: QbField('metadata', dtype=typing.Dict[str, typing.Any]) +node_pk: QbField('node_pk', 'dbnode_id', dtype=int) +pk: QbField('pk', 'id', dtype=int) +time: QbField('time', dtype=datetime) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_User.yml b/tests/orm/test_fields/fields_User.yml new file mode 100644 index 0000000000..0030402238 --- /dev/null +++ b/tests/orm/test_fields/fields_User.yml @@ -0,0 +1,5 @@ +email: QbField('email', dtype=str) +first_name: QbField('first_name', dtype=str) +institution: QbField('institution', dtype=str) +last_name: QbField('last_name', dtype=str) +pk: QbField('pk', 'id', dtype=int) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.array.ArrayData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml new file mode 100644 index 0000000000..80e6bef74b --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.array.bands.BandsData.yml @@ -0,0 +1,21 @@ +array_labels: QbAttrField('array_labels', dtype=typing.Union[typing.List[str],None]) +cell: QbAttrField('cell', dtype=typing.List[typing.List[float]]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +label_numbers: QbAttrField('label_numbers', dtype=typing.List[int]) +labels: QbAttrField('labels', dtype=typing.List[str]) +mesh: QbAttrField('mesh', dtype=typing.List[int]) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +offset: QbAttrField('offset', dtype=typing.List[float]) +pbc1: QbAttrField('pbc1', dtype=bool) +pbc2: QbAttrField('pbc2', dtype=bool) +pbc3: QbAttrField('pbc3', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +units: QbAttrField('units', dtype=str) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml new file mode 100644 index 0000000000..499f879809 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.array.kpoints.KpointsData.yml @@ -0,0 +1,19 @@ +cell: QbAttrField('cell', dtype=typing.List[typing.List[float]]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +label_numbers: QbAttrField('label_numbers', dtype=typing.List[int]) +labels: QbAttrField('labels', dtype=typing.List[str]) +mesh: QbAttrField('mesh', dtype=typing.List[int]) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +offset: QbAttrField('offset', dtype=typing.List[float]) +pbc1: QbAttrField('pbc1', dtype=bool) +pbc2: QbAttrField('pbc2', dtype=bool) +pbc3: QbAttrField('pbc3', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.array.projection.ProjectionData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml new file mode 100644 index 0000000000..aec85a4ae4 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.array.trajectory.TrajectoryData.yml @@ -0,0 +1,14 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +symbols: QbAttrField('symbols', dtype=typing.List[str]) +units_positions: QbAttrField('units_positions', 'units|positions', dtype=str) +units_times: QbAttrField('units_times', 'units|times', dtype=str) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.array.xy.XyData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml b/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml new file mode 100644 index 0000000000..e247a0e0f6 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.base.BaseType.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) +value: QbAttrField('value') diff --git a/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml b/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml new file mode 100644 index 0000000000..e247a0e0f6 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.bool.Bool.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) +value: QbAttrField('value') diff --git a/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml b/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml new file mode 100644 index 0000000000..a68dee1951 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.cif.CifData.yml @@ -0,0 +1,14 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +formulae: QbAttrField('formulae', dtype=typing.List[str]) +label: QbField('label', dtype=str) +md5: QbAttrField('md5', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +spacegroup_numbers: QbAttrField('spacegroup_numbers', dtype=typing.List[str]) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml b/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml new file mode 100644 index 0000000000..a91a703bc9 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.code.Code.yml @@ -0,0 +1,17 @@ +append_text: QbAttrField('append_text', dtype=typing.Union[str,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +input_plugin: QbAttrField('input_plugin', dtype=typing.Union[str,None]) +is_local: QbAttrField('is_local', dtype=typing.Union[bool,None]) +label: QbField('label', dtype=str) +local_executable: QbAttrField('local_executable', dtype=typing.Union[str,None]) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +prepend_text: QbAttrField('prepend_text', dtype=typing.Union[str,None]) +remote_exec_path: QbAttrField('remote_exec_path', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml b/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml new file mode 100644 index 0000000000..e5de0268b5 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.dict.Dict.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +dict: QbField('dict', 'attributes', dtype=typing.Dict[str, typing.Any], subscriptable=True) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml b/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.enum.EnumData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml b/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml new file mode 100644 index 0000000000..e247a0e0f6 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.float.Float.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) +value: QbAttrField('value') diff --git a/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml b/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.folder.FolderData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml b/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml new file mode 100644 index 0000000000..e247a0e0f6 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.int.Int.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) +value: QbAttrField('value') diff --git a/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml b/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.jsonable.JsonableData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.list.List.yml b/tests/orm/test_fields/fields_aiida.data.core.list.List.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.list.List.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml b/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml new file mode 100644 index 0000000000..e247a0e0f6 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.numeric.NumericType.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) +value: QbAttrField('value') diff --git a/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml b/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.orbital.OrbitalData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml new file mode 100644 index 0000000000..401e98149a --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.RemoteData.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +remote_path: QbAttrField('remote_path', dtype=str) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml new file mode 100644 index 0000000000..6f9b4211bd --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.RemoteStashData.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +stash_mode: QbAttrField('stash_mode', dtype=str) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml new file mode 100644 index 0000000000..082fcb5703 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.remote.stash.folder.RemoteStashFolderData.yml @@ -0,0 +1,14 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +source_list: QbAttrField('source_list', dtype=typing.List[str]) +stash_mode: QbAttrField('stash_mode', dtype=str) +target_basepath: QbAttrField('target_basepath', dtype=str) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml b/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.singlefile.SinglefileData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml b/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml new file mode 100644 index 0000000000..e247a0e0f6 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.str.Str.yml @@ -0,0 +1,12 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) +value: QbAttrField('value') diff --git a/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml new file mode 100644 index 0000000000..eaf7c67277 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.structure.StructureData.yml @@ -0,0 +1,17 @@ +cell: QbAttrField('cell', dtype=typing.List[typing.List[float]]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +kinds: QbAttrField('kinds', dtype=typing.Union[typing.List[dict],None]) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pbc1: QbAttrField('pbc1', dtype=bool) +pbc2: QbAttrField('pbc2', dtype=bool) +pbc3: QbAttrField('pbc3', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sites: QbAttrField('sites', dtype=typing.Union[typing.List[dict],None]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml b/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.data.core.upf.UpfData.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.data.Data.yml b/tests/orm/test_fields/fields_aiida.node.data.Data.yml new file mode 100644 index 0000000000..132bffedc0 --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.data.Data.yml @@ -0,0 +1,11 @@ +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +pk: QbField('pk', 'id', dtype=int) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +source: QbAttrField('source', dtype=typing.Union[dict,None], subscriptable=True) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml b/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml new file mode 100644 index 0000000000..635aa8f97d --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.ProcessNode.yml @@ -0,0 +1,20 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sealed: QbAttrField('sealed', dtype=bool) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml b/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml new file mode 100644 index 0000000000..635aa8f97d --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.calculation.CalculationNode.yml @@ -0,0 +1,20 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sealed: QbAttrField('sealed', dtype=bool) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml new file mode 100644 index 0000000000..635aa8f97d --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcfunction.CalcFunctionNode.yml @@ -0,0 +1,20 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sealed: QbAttrField('sealed', dtype=bool) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml new file mode 100644 index 0000000000..ec869a30fa --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.calculation.calcjob.CalcJobNode.yml @@ -0,0 +1,30 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +detailed_job_info: QbAttrField('detailed_job_info', dtype=typing.Union[dict,None]) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +imported: QbAttrField('imported', dtype=typing.Union[bool,None]) +job_id: QbAttrField('job_id', dtype=typing.Union[str,None]) +label: QbField('label', dtype=str) +last_job_info: QbAttrField('last_job_info', dtype=typing.Union[str,None]) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +remote_workdir: QbAttrField('remote_workdir', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +retrieve_list: QbAttrField('retrieve_list', dtype=typing.Union[typing.List[str],None]) +retrieve_temporary_list: QbAttrField('retrieve_temporary_list', dtype=typing.Union[typing.List[str],None]) +scheduler_lastchecktime: QbAttrField('scheduler_lastchecktime', dtype=typing.Union[str,None]) +scheduler_state: QbAttrField('scheduler_state', dtype=typing.Union[str,None]) +sealed: QbAttrField('sealed', dtype=bool) +state: QbAttrField('state', dtype=typing.Union[str,None]) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml b/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml new file mode 100644 index 0000000000..635aa8f97d --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.workflow.WorkflowNode.yml @@ -0,0 +1,20 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sealed: QbAttrField('sealed', dtype=bool) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml b/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml new file mode 100644 index 0000000000..635aa8f97d --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.workflow.workchain.WorkChainNode.yml @@ -0,0 +1,20 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sealed: QbAttrField('sealed', dtype=bool) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml b/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml new file mode 100644 index 0000000000..635aa8f97d --- /dev/null +++ b/tests/orm/test_fields/fields_aiida.node.process.workflow.workfunction.WorkFunctionNode.yml @@ -0,0 +1,20 @@ +computer_pk: QbField('computer_pk', 'dbcomputer_id', dtype=typing.Union[int,None]) +ctime: QbField('ctime', dtype=datetime) +description: QbField('description', dtype=str) +exception: QbAttrField('exception', dtype=typing.Union[str,None]) +exit_message: QbAttrField('exit_message', dtype=typing.Union[str,None]) +exit_status: QbAttrField('exit_status', dtype=typing.Union[int,None]) +extras: QbField('extras', dtype=typing.Dict[str, typing.Any], subscriptable=True) +label: QbField('label', dtype=str) +mtime: QbField('mtime', dtype=datetime) +node_type: QbField('node_type', dtype=str) +paused: QbAttrField('paused', dtype=bool) +pk: QbField('pk', 'id', dtype=int) +process_label: QbAttrField('process_label', dtype=typing.Union[str,None]) +process_state: QbAttrField('process_state', dtype=typing.Union[str,None]) +process_status: QbAttrField('process_status', dtype=typing.Union[str,None]) +process_type: QbField('process_type', dtype=typing.Union[str,None]) +repository_metadata: QbField('repository_metadata', dtype=typing.Dict[str, typing.Any]) +sealed: QbAttrField('sealed', dtype=bool) +user_pk: QbField('user_pk', 'user_id', dtype=int) +uuid: QbField('uuid', dtype=str) diff --git a/tests/orm/test_querybuilder/test_as_dict.yml b/tests/orm/test_querybuilder/test_as_dict.yml index 8d5650cbe5..2a453799a4 100644 --- a/tests/orm/test_querybuilder/test_as_dict.yml +++ b/tests/orm/test_querybuilder/test_as_dict.yml @@ -17,3 +17,4 @@ path: tag: node_1 project: node_1: [] +project_map: {} diff --git a/tests/orm/test_querybuilder/test_str.txt b/tests/orm/test_querybuilder/test_str.txt index 40e518f489..9f57b200b8 100644 --- a/tests/orm/test_querybuilder/test_str.txt +++ b/tests/orm/test_querybuilder/test_str.txt @@ -1 +1 @@ -QueryBuilder(path=[{'entity_type': 'data.Data.', 'orm_base': 'node', 'tag': 'Data_1', 'joining_keyword': None, 'joining_value': None, 'edge_tag': None, 'outerjoin': False}], filters={'Data_1': {'node_type': {'like': 'data.%'}}}, project={'Data_1': [{'id': {}}, {'uuid': {}}]}, order_by=[{'Data_1': [{'id': {'order': 'asc'}}]}], limit=None, offset=None, distinct=False) \ No newline at end of file +QueryBuilder(path=[{'entity_type': 'data.Data.', 'orm_base': 'node', 'tag': 'Data_1', 'joining_keyword': None, 'joining_value': None, 'edge_tag': None, 'outerjoin': False}], filters={'Data_1': {'node_type': {'like': 'data.%'}}}, project={'Data_1': [{'id': {}}, {'uuid': {}}]}, project_map={}, order_by=[{'Data_1': [{'id': {'order': 'asc'}}]}], limit=None, offset=None, distinct=False) \ No newline at end of file