You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ts...@apache.org on 2013/04/25 16:46:11 UTC
[45/51] [abbrv] git commit: updated refs/heads/marvin_refactor to
dbcfc66
changes to the entity_generator
adding support for capturing plurals
adding filtering for non-conforming entities
adding more error checking in entity.action {return}
Signed-off-by: Prasanna Santhanam <ts...@apache.org>
Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/dce00d43
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/dce00d43
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/dce00d43
Branch: refs/heads/marvin_refactor
Commit: dce00d43e1c1cc59a5e6478887ab12ff769f9c1e
Parents: a2318d2
Author: Prasanna Santhanam <ts...@apache.org>
Authored: Thu Apr 25 16:57:17 2013 +0530
Committer: Prasanna Santhanam <ts...@apache.org>
Committed: Thu Apr 25 16:57:17 2013 +0530
----------------------------------------------------------------------
tools/marvin/marvin/cs_entity_generator.py | 213 ++++++++++++++---------
tools/marvin/marvin/generateBase.py | 202 ---------------------
2 files changed, 132 insertions(+), 283 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/dce00d43/tools/marvin/marvin/cs_entity_generator.py
----------------------------------------------------------------------
diff --git a/tools/marvin/marvin/cs_entity_generator.py b/tools/marvin/marvin/cs_entity_generator.py
index f6d94c0..c4592ad 100644
--- a/tools/marvin/marvin/cs_entity_generator.py
+++ b/tools/marvin/marvin/cs_entity_generator.py
@@ -15,20 +15,21 @@
# specific language governing permissions and limitations
# under the License.
-import marvin
from marvin.cloudstackAPI import *
import os
+import inflect
-# Add verbs in grammar - same as cloudmonkey
+# Grammar for CloudStack APIs
grammar = ['create', 'list', 'delete', 'update',
'enable', 'activate', 'disable', 'add', 'remove',
- 'attach', 'detach', 'associate', 'generate', 'ldap',
- 'assign', 'authorize', 'change', 'register', 'configure',
+ 'attach', 'detach', 'associate', 'generate', 'assign',
+ 'authorize', 'change', 'register', 'configure',
'start', 'restart', 'reboot', 'stop', 'reconnect',
'cancel', 'destroy', 'revoke', 'mark', 'reset',
'copy', 'extract', 'migrate', 'restore', 'suspend',
'get', 'query', 'prepare', 'deploy', 'upload', 'lock',
- 'disassociate', 'scale']
+ 'disassociate', 'scale', 'dedicate', 'archive', 'find',
+ 'recover', 'release', 'resize', 'revert']
LICENSE = """# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
@@ -48,105 +49,155 @@ LICENSE = """# Licensed to the Apache Software Foundation (ASF) under one
# under the License.
"""
-
def get_api_cmds():
+ """@return: instances of all the API commands exposed by CloudStack
+ """
api_classes = __import__('marvin.cloudstackAPI')
-
cmdlist = map(
- lambda f: getattr(api_classes.cloudstackAPI, f),
- filter(
- lambda t: t.startswith('__') == False,
- dir(api_classes.cloudstackAPI)
- )
+ lambda f: getattr(api_classes.cloudstackAPI, f),
+ filter(
+ lambda t: t.startswith('__') == False,
+ dir(api_classes.cloudstackAPI)
+ )
)
-
cmdlist = filter(
- lambda g: g is not None,
- cmdlist
+ lambda g: g is not None,
+ cmdlist
)
-
clslist = map(
- lambda g: getattr(g, g.__name__.split('.')[-1] + 'Cmd'),
- filter(
- lambda h: h.__name__.split('.')[-1] not in ['baseCmd', 'baseResponse', 'cloudstackAPIClient'],
- cmdlist
- )
+ lambda g: getattr(g, g.__name__.split('.')[-1] + 'Cmd'),
+ filter(
+ lambda h: h.__name__.split('.')[-1] not in ['baseCmd', 'baseResponse', 'cloudstackAPIClient'],
+ cmdlist
+ )
)
-
cmdlets = map(lambda t: t(), clslist)
return cmdlets
-def get_entity_from_api(api):
- matching_verbs = filter(lambda v: api.__class__.__name__.startswith(v), grammar)
+
+def singularize(word, num=0):
+ """Use the inflect engine to make singular nouns of the entities
+ @return: singular of `word`
+ """
+ inflector = inflect.engine()
+ return inflector.singular_noun(word, num)
+
+
+def transform_entity(entity):
+ if entity == 'DefaultZoneForAccount':
+ #markDefaultZoneForAccount -> Zone
+ return 'Zone'
+ elif entity in ['ToGlobalLoadBalancerRule', 'FromGlobalLoadBalancerRule']:
+ return 'GlobalLoadBalancerRule'
+ elif entity in ['AccountToProject', 'AccountFromProject']:
+ return 'Project'
+ elif entity in ['ToSnapshot']:
+ return 'Snapshot'
+ return entity
+
+
+def skip_list():
+ """Entities and APIs that we will not auto-generate
+ """
+ return ['ldapConfigCmd', 'ldapRemoveCmd']
+
+
+def get_verb_and_entity(api):
+ """Break down the API cmd instance in to `verb` and `Entity`
+ @return: verb, Entity tuple
+ """
+ matching_verbs = filter(lambda v: api.startswith(v), grammar)
if len(matching_verbs) > 0:
verb = matching_verbs[0]
- entity = api.__class__.__name__.replace(verb, '').replace('Cmd', '')
- return entity
+ entity = api.replace(verb, '').replace('Cmd', '')
+ entity = transform_entity(entity)
+ return verb, singularize(entity)
+ else:
+ print "No matching verb, entity breakdown for api %s" % api
+
def get_actionable_entities():
- cmdlets = sorted(get_api_cmds(), key=lambda k: get_entity_from_api(k))
+ """
+ Inspect all entities and return a map of the Entity against the actions
+ along with the required arguments to satisfy the action
+ @return: Dictionary of Entity { "verb" : [required] }
+ """
+ cmdlets = sorted(get_api_cmds(), key=lambda k: get_verb_and_entity(k.__class__.__name__))
entities = {}
for cmd in cmdlets:
requireds = getattr(cmd, 'required')
optionals = filter(lambda x: '__' not in x and 'required' not in x and 'isAsync' not in x, dir(cmd))
- matching_verbs = filter(lambda v: cmd.__class__.__name__.startswith(v), grammar)
- if len(matching_verbs)> 0:
- verb = matching_verbs[0]
- entity = cmd.__class__.__name__.replace(verb, '').replace('Cmd', '')
- if entity[:-1] in entities:
- # Accounts and Account are the same entity
- entity = entity[:-1]
- if entity[:-2] in entities:
- # IpAddresses and IpAddress are the same entity
- entity = entity[:-2]
- if entity not in entities:
- entities[entity] = { }
- entities[entity][verb] = {}
- entities[entity][verb]['args'] = requireds
- entities[entity][verb]['apimodule'] = cmd.__class__.__module__.split('.')[-1]
- entities[entity][verb]['apicmd'] = cmd.__class__.__name__
+ api = cmd.__class__.__name__
+ if api in skip_list():
+ continue
+ verb, entity = get_verb_and_entity(api)
+ if entity not in entities:
+ entities[entity] = {}
+ entities[entity][verb] = {}
+ entities[entity][verb]['args'] = requireds
+ entities[entity][verb]['apimodule'] = cmd.__class__.__module__.split('.')[-1]
+ entities[entity][verb]['apicmd'] = api
return entities
-def write_entity_classes(entities):
+
+def write_entity_classes(entities, module=None):
+ """
+ Writes the collected entity classes into `path`
+ @param entities: dictionary of entities and the verbs acting on them
+ @param module: top level module to the generated classes
+ @return:
+ """
tabspace = ' '
entitydict = {}
- #TODO: Add license header for ASLv2
for entity, actions in entities.iteritems():
body = []
imports = []
- imports.append('from marvin.integration.lib.base import CloudStackEntity')
- body.append('class %s(CloudStackEntity.CloudStackEntity):'%entity)
- body.append('\n')
+ imports.append('from %s.CloudStackEntity import CloudStackEntity' % module)
+ body.append('class %s(CloudStackEntity):' % entity)
+ #TODO: Add docs for entity
+ body.append('\n\n')
body.append(tabspace + 'def __init__(self, items):')
- body.append(tabspace*2 + 'self.__dict__.update(items)')
+ body.append(tabspace * 2 + 'self.__dict__.update(items)')
body.append('\n')
for action, details in actions.iteritems():
- imports.append('from marvin.cloudstackAPI import %s'%details['apimodule'])
+ imports.append('from marvin.cloudstackAPI import %s' % details['apimodule'])
if action in ['create', 'list', 'deploy']:
body.append(tabspace + '@classmethod')
- if action in ['create', 'deploy']:
- body.append(tabspace + 'def %s(cls, apiclient, %sFactory, **kwargs):'%(action, entity))
- body.append(tabspace*2 + 'cmd = %(module)s.%(command)s()'%{"module": details["apimodule"], "command": details["apicmd"]})
- body.append(tabspace*2 + '[setattr(cmd, factoryKey, factoryValue) for factoryKey, factoryValue in %sFactory.__dict__.iteritems()]'%entity)
- body.append(tabspace*2 + '[setattr(cmd, key, value) for key,value in kwargs.iteritems()]')
- body.append(tabspace*2 + '%s = apiclient.%s(cmd)'%(entity.lower(), details['apimodule']))
- body.append(tabspace*2 + 'return %s(%s.__dict__)'%(entity, entity.lower()))
- else:
+ if action not in ['create', 'deploy']:
if len(details['args']) > 0:
- body.append(tabspace + 'def %s(self, apiclient, %s, **kwargs):'%(action, ', '.join(list(set(details['args'])))))
+ body.append(tabspace + 'def %s(self, apiclient, %s, **kwargs):' % (
+ action, ', '.join(list(set(details['args'])))))
else:
- body.append(tabspace + 'def %s(self, apiclient, **kwargs):'%(action))
- body.append(tabspace*2 + 'cmd = %(module)s.%(command)s()'%{"module": details["apimodule"], "command": details["apicmd"]})
+ body.append(tabspace + 'def %s(self, apiclient, **kwargs):' % (action))
+ body.append(tabspace * 2 + 'cmd = %(module)s.%(command)s()' % {"module": details["apimodule"],
+ "command": details["apicmd"]})
if action not in ['create', 'list', 'deploy']:
- body.append(tabspace*2 + 'cmd.id = self.id')
+ body.append(tabspace * 2 + 'cmd.id = self.id')
for arg in details['args']:
- body.append(tabspace*2 + 'cmd.%s = %s'%(arg, arg))
- body.append(tabspace*2 + '[setattr(cmd, key, value) for key,value in kwargs.iteritems()]')
- body.append(tabspace*2 + '%s = apiclient.%s(cmd)'%(entity.lower(), details['apimodule']))
+ body.append(tabspace * 2 + 'cmd.%s = %s' % (arg, arg))
+ body.append(tabspace * 2 + '[setattr(cmd, key, value) for key,value in kwargs.iteritems()]')
+ body.append(tabspace * 2 + '%s = apiclient.%s(cmd)' % (entity.lower(), details['apimodule']))
if action in ['list']:
- body.append(tabspace*2 + 'return map(lambda e: %s(e.__dict__), %s)'%(entity, entity.lower()))
+ body.append(
+ tabspace * 2 + 'return map(lambda e: %s(e.__dict__), %s) if %s and len(%s) > 0 else None' % (
+ entity, entity.lower(), entity.lower(), entity.lower()))
else:
- body.append(tabspace*2 + 'return %s'%(entity.lower()))
+ body.append(tabspace * 2 + 'return %s if %s else None' % (entity.lower(), entity.lower()))
+ else:
+ body.append(tabspace + 'def %s(cls, apiclient, %s, factory=None, **kwargs):' % (
+ action, ', '.join(map(lambda arg: arg + '=None', list(set(details['args'])))), entity))
+ #TODO: Add docs for actions
+ body.append(tabspace * 2 + 'cmd = %(module)s.%(command)s()' % {"module": details["apimodule"],
+ "command": details["apicmd"]})
+ body.append(tabspace * 2 + 'if factory:')
+ body.append(
+ tabspace * 3 + '[setattr(cmd, factoryKey, factoryValue) for factoryKey, factoryValue in %sFactory.__dict__.iteritems()]' % entity)
+ body.append(tabspace * 2 + 'else:')
+ for arg in details["args"]:
+ body.append(tabspace * 3 + "cmd.%s = %s" % (arg, arg))
+ body.append(tabspace * 2 + '[setattr(cmd, key, value) for key, value in kwargs.iteritems()]')
+ body.append(tabspace * 2 + '%s = apiclient.%s(cmd)' % (entity.lower(), details['apimodule']))
+ body.append(tabspace * 2 + 'return %s(%s.__dict__) if %s else None' % (entity, entity.lower(), entity.lower()))
body.append('\n')
imports = '\n'.join(imports)
@@ -154,15 +205,15 @@ def write_entity_classes(entities):
code = imports + '\n\n' + body
entitydict[entity] = code
- #write_entity_factory(entity, actions)
- with open("./base/%s.py"%entity, "w") as writer:
+ #write_entity_factory(entity, actions, path)
+ module_path = '/'.join(module.split('.'))[1:]
+ with open(".%s/%s.py" % (module_path, entity), "w") as writer:
writer.write(LICENSE)
writer.write(code)
def write_entity_factory(entity, actions):
tabspace = ' '
- #TODO: Add license header for ASLv2
code = ''
factory_defaults = []
if 'create' in actions:
@@ -176,27 +227,27 @@ def write_entity_factory(entity, actions):
else:
return
- if os.path.exists("./factory/%sFactory.py"%entity):
+ if os.path.exists("./factory/%sFactory.py" % entity):
for arg in factory_defaults:
- code += tabspace + '%s = None\n'%arg
- with open("./factory/%sFactory.py"%entity, "r") as reader:
+ code += tabspace + '%s = None\n' % arg
+ with open("./factory/%sFactory.py" % entity, "r") as reader:
rcode = reader.read()
if rcode.find(code) > 0:
return
- with open("./factory/%sFactory.py"%entity, "a") as writer:
+ with open("./factory/%sFactory.py" % entity, "a") as writer:
writer.write(code)
else:
code += 'import factory\n'
- code += 'from marvin.integration.lib.base import %s\n'%entity
- code += 'class %sFactory(factory.Factory):'%entity
+ code += 'from marvin.integration.lib.base import %s\n' % entity
+ code += 'class %sFactory(factory.Factory):' % entity
code += '\n\n'
- code += tabspace + 'FACTORY_FOR = %s.%s\n\n'%(entity,entity)
+ code += tabspace + 'FACTORY_FOR = %s.%s\n\n' % (entity, entity)
for arg in factory_defaults:
- code += tabspace + '%s = None\n'%arg
- with open("./factory/%sFactory.py"%entity, "w") as writer:
+ code += tabspace + '%s = None\n' % arg
+ with open("./factory/%sFactory.py" % entity, "w") as writer:
writer.write(LICENSE)
writer.write(code)
-if __name__=='__main__':
+if __name__ == '__main__':
entities = get_actionable_entities()
- write_entity_classes(entities)
+ write_entity_classes(entities, 'base2')
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/dce00d43/tools/marvin/marvin/generateBase.py
----------------------------------------------------------------------
diff --git a/tools/marvin/marvin/generateBase.py b/tools/marvin/marvin/generateBase.py
deleted file mode 100644
index f6d94c0..0000000
--- a/tools/marvin/marvin/generateBase.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-import marvin
-from marvin.cloudstackAPI import *
-import os
-
-# Add verbs in grammar - same as cloudmonkey
-grammar = ['create', 'list', 'delete', 'update',
- 'enable', 'activate', 'disable', 'add', 'remove',
- 'attach', 'detach', 'associate', 'generate', 'ldap',
- 'assign', 'authorize', 'change', 'register', 'configure',
- 'start', 'restart', 'reboot', 'stop', 'reconnect',
- 'cancel', 'destroy', 'revoke', 'mark', 'reset',
- 'copy', 'extract', 'migrate', 'restore', 'suspend',
- 'get', 'query', 'prepare', 'deploy', 'upload', 'lock',
- 'disassociate', 'scale']
-
-LICENSE = """# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-"""
-
-
-def get_api_cmds():
- api_classes = __import__('marvin.cloudstackAPI')
-
- cmdlist = map(
- lambda f: getattr(api_classes.cloudstackAPI, f),
- filter(
- lambda t: t.startswith('__') == False,
- dir(api_classes.cloudstackAPI)
- )
- )
-
- cmdlist = filter(
- lambda g: g is not None,
- cmdlist
- )
-
- clslist = map(
- lambda g: getattr(g, g.__name__.split('.')[-1] + 'Cmd'),
- filter(
- lambda h: h.__name__.split('.')[-1] not in ['baseCmd', 'baseResponse', 'cloudstackAPIClient'],
- cmdlist
- )
- )
-
- cmdlets = map(lambda t: t(), clslist)
- return cmdlets
-
-def get_entity_from_api(api):
- matching_verbs = filter(lambda v: api.__class__.__name__.startswith(v), grammar)
- if len(matching_verbs) > 0:
- verb = matching_verbs[0]
- entity = api.__class__.__name__.replace(verb, '').replace('Cmd', '')
- return entity
-
-def get_actionable_entities():
- cmdlets = sorted(get_api_cmds(), key=lambda k: get_entity_from_api(k))
- entities = {}
- for cmd in cmdlets:
- requireds = getattr(cmd, 'required')
- optionals = filter(lambda x: '__' not in x and 'required' not in x and 'isAsync' not in x, dir(cmd))
- matching_verbs = filter(lambda v: cmd.__class__.__name__.startswith(v), grammar)
- if len(matching_verbs)> 0:
- verb = matching_verbs[0]
- entity = cmd.__class__.__name__.replace(verb, '').replace('Cmd', '')
- if entity[:-1] in entities:
- # Accounts and Account are the same entity
- entity = entity[:-1]
- if entity[:-2] in entities:
- # IpAddresses and IpAddress are the same entity
- entity = entity[:-2]
- if entity not in entities:
- entities[entity] = { }
- entities[entity][verb] = {}
- entities[entity][verb]['args'] = requireds
- entities[entity][verb]['apimodule'] = cmd.__class__.__module__.split('.')[-1]
- entities[entity][verb]['apicmd'] = cmd.__class__.__name__
- return entities
-
-def write_entity_classes(entities):
- tabspace = ' '
- entitydict = {}
- #TODO: Add license header for ASLv2
- for entity, actions in entities.iteritems():
- body = []
- imports = []
- imports.append('from marvin.integration.lib.base import CloudStackEntity')
- body.append('class %s(CloudStackEntity.CloudStackEntity):'%entity)
- body.append('\n')
- body.append(tabspace + 'def __init__(self, items):')
- body.append(tabspace*2 + 'self.__dict__.update(items)')
- body.append('\n')
- for action, details in actions.iteritems():
- imports.append('from marvin.cloudstackAPI import %s'%details['apimodule'])
- if action in ['create', 'list', 'deploy']:
- body.append(tabspace + '@classmethod')
- if action in ['create', 'deploy']:
- body.append(tabspace + 'def %s(cls, apiclient, %sFactory, **kwargs):'%(action, entity))
- body.append(tabspace*2 + 'cmd = %(module)s.%(command)s()'%{"module": details["apimodule"], "command": details["apicmd"]})
- body.append(tabspace*2 + '[setattr(cmd, factoryKey, factoryValue) for factoryKey, factoryValue in %sFactory.__dict__.iteritems()]'%entity)
- body.append(tabspace*2 + '[setattr(cmd, key, value) for key,value in kwargs.iteritems()]')
- body.append(tabspace*2 + '%s = apiclient.%s(cmd)'%(entity.lower(), details['apimodule']))
- body.append(tabspace*2 + 'return %s(%s.__dict__)'%(entity, entity.lower()))
- else:
- if len(details['args']) > 0:
- body.append(tabspace + 'def %s(self, apiclient, %s, **kwargs):'%(action, ', '.join(list(set(details['args'])))))
- else:
- body.append(tabspace + 'def %s(self, apiclient, **kwargs):'%(action))
- body.append(tabspace*2 + 'cmd = %(module)s.%(command)s()'%{"module": details["apimodule"], "command": details["apicmd"]})
- if action not in ['create', 'list', 'deploy']:
- body.append(tabspace*2 + 'cmd.id = self.id')
- for arg in details['args']:
- body.append(tabspace*2 + 'cmd.%s = %s'%(arg, arg))
- body.append(tabspace*2 + '[setattr(cmd, key, value) for key,value in kwargs.iteritems()]')
- body.append(tabspace*2 + '%s = apiclient.%s(cmd)'%(entity.lower(), details['apimodule']))
- if action in ['list']:
- body.append(tabspace*2 + 'return map(lambda e: %s(e.__dict__), %s)'%(entity, entity.lower()))
- else:
- body.append(tabspace*2 + 'return %s'%(entity.lower()))
- body.append('\n')
-
- imports = '\n'.join(imports)
- body = '\n'.join(body)
- code = imports + '\n\n' + body
-
- entitydict[entity] = code
- #write_entity_factory(entity, actions)
- with open("./base/%s.py"%entity, "w") as writer:
- writer.write(LICENSE)
- writer.write(code)
-
-
-def write_entity_factory(entity, actions):
- tabspace = ' '
- #TODO: Add license header for ASLv2
- code = ''
- factory_defaults = []
- if 'create' in actions:
- factory_defaults.extend(actions['create']['args'])
- elif 'deploy' in actions:
- factory_defaults.extend(actions['deploy']['args'])
- elif 'associate' in actions:
- factory_defaults.extend(actions['associate']['args'])
- elif 'register' in actions:
- factory_defaults.extend(actions['register']['args'])
- else:
- return
-
- if os.path.exists("./factory/%sFactory.py"%entity):
- for arg in factory_defaults:
- code += tabspace + '%s = None\n'%arg
- with open("./factory/%sFactory.py"%entity, "r") as reader:
- rcode = reader.read()
- if rcode.find(code) > 0:
- return
- with open("./factory/%sFactory.py"%entity, "a") as writer:
- writer.write(code)
- else:
- code += 'import factory\n'
- code += 'from marvin.integration.lib.base import %s\n'%entity
- code += 'class %sFactory(factory.Factory):'%entity
- code += '\n\n'
- code += tabspace + 'FACTORY_FOR = %s.%s\n\n'%(entity,entity)
- for arg in factory_defaults:
- code += tabspace + '%s = None\n'%arg
- with open("./factory/%sFactory.py"%entity, "w") as writer:
- writer.write(LICENSE)
- writer.write(code)
-
-if __name__=='__main__':
- entities = get_actionable_entities()
- write_entity_classes(entities)