Fuel UI
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013 Mirantis, Inc.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. """
  16. Base classes for objects and collections
  17. """
  18. import collections
  19. import functools
  20. from itertools import ifilter
  21. import operator
  22. from oslo_serialization import jsonutils
  23. import six
  24. from sqlalchemy import and_, not_
  25. from sqlalchemy.orm import joinedload
  26. from sqlalchemy.orm import Query
  27. from nailgun.objects.serializers.base import BasicSerializer
  28. from nailgun.db import db
  29. from nailgun import errors
  30. class NailgunObject(object):
  31. """Base class for objects"""
  32. #: Serializer class for object
  33. serializer = BasicSerializer
  34. #: SQLAlchemy model for object
  35. model = None
  36. @classmethod
  37. def get_by_uid(cls, uid, fail_if_not_found=False, lock_for_update=False):
  38. """Get instance by it's uid (PK in case of SQLAlchemy)
  39. :param uid: uid of object
  40. :param fail_if_not_found: raise an exception if object is not found
  41. :param lock_for_update: lock returned object for update (DB mutex)
  42. :returns: instance of an object (model)
  43. """
  44. q = db().query(cls.model)
  45. if lock_for_update:
  46. # todo(ikutukov): replace to the with_for_update
  47. # http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.
  48. # orm.query.Query.with_for_update
  49. q = q.with_lockmode('update')
  50. res = q.get(uid)
  51. if not res and fail_if_not_found:
  52. raise errors.ObjectNotFound(
  53. "Object '{0}' with UID={1} is not found in DB".format(
  54. cls.__name__,
  55. uid
  56. )
  57. )
  58. return res
  59. @classmethod
  60. def create(cls, data):
  61. """Create object instance with specified parameters in DB
  62. :param data: dictionary of key-value pairs as object fields
  63. :returns: instance of an object (model)
  64. """
  65. new_obj = cls.model()
  66. for key, value in six.iteritems(data):
  67. setattr(new_obj, key, value)
  68. db().add(new_obj)
  69. db().flush()
  70. return new_obj
  71. @classmethod
  72. def update(cls, instance, data):
  73. """Update existing instance with specified parameters
  74. :param instance: object (model) instance
  75. :param data: dictionary of key-value pairs as object fields
  76. :returns: instance of an object (model)
  77. """
  78. instance.update(data)
  79. db().add(instance)
  80. db().flush()
  81. return instance
  82. @classmethod
  83. def delete(cls, instance):
  84. """Delete object (model) instance
  85. :param instance: object (model) instance
  86. :returns: None
  87. """
  88. db().delete(instance)
  89. db().flush()
  90. @classmethod
  91. def bulk_delete(cls, instance_ids):
  92. db().query(cls.model).filter(
  93. cls.model.id.in_(instance_ids)).delete(synchronize_session='fetch')
  94. @classmethod
  95. def save(cls, instance=None):
  96. """Save current changes for instance in DB.
  97. Current transaction will be commited
  98. (in case of SQLAlchemy).
  99. :param instance: object (model) instance
  100. :returns: None
  101. """
  102. if instance:
  103. db().add(instance)
  104. db().commit()
  105. @classmethod
  106. def to_dict(cls, instance, fields=None, serializer=None):
  107. """Serialize instance to Python dict
  108. :param instance: object (model) instance
  109. :param fields: exact fields to serialize
  110. :param serializer: the custom serializer
  111. :returns: serialized object (model) as dictionary
  112. """
  113. serializer = serializer or cls.serializer
  114. return serializer.serialize(instance, fields=fields)
  115. @classmethod
  116. def to_json(cls, instance, fields=None, serializer=None):
  117. """Serialize instance to JSON
  118. :param instance: object (model) instance
  119. :param fields: exact fields to serialize
  120. :param serializer: the custom serializer
  121. :returns: serialized object (model) as JSON string
  122. """
  123. return jsonutils.dumps(
  124. cls.to_dict(instance, fields=fields, serializer=serializer)
  125. )
  126. @classmethod
  127. def compare(cls, instance, other, order_by):
  128. for field in order_by:
  129. a = getattr(instance, field['name'])
  130. b = getattr(other, field['name'])
  131. if a < b:
  132. return field['lt']
  133. elif a > b:
  134. return field['gt']
  135. return 0
  136. class NailgunCollection(object):
  137. """Base class for object collections"""
  138. #: Single object class
  139. single = NailgunObject
  140. @classmethod
  141. def _is_iterable(cls, obj):
  142. return isinstance(
  143. obj,
  144. collections.Iterable
  145. )
  146. @classmethod
  147. def _is_query(cls, obj):
  148. return isinstance(
  149. obj,
  150. Query
  151. )
  152. @classmethod
  153. def all(cls):
  154. """Get all instances of this object (model)
  155. :returns: iterable (SQLAlchemy query)
  156. """
  157. return db().query(cls.single.model)
  158. @classmethod
  159. def _query_order_by(cls, query, order_by):
  160. """Adds order by clause into SQLAlchemy query
  161. :param query: SQLAlchemy query
  162. :param order_by: tuple of model fields names for ORDER BY criterion
  163. to SQLAlchemy query. If name starts with '-' desc ordering applies,
  164. else asc.
  165. """
  166. order_args = []
  167. for field_name in order_by:
  168. if field_name.startswith('-'):
  169. field_name = field_name.lstrip('-')
  170. ordering = 'desc'
  171. else:
  172. ordering = 'asc'
  173. field = getattr(cls.single.model, field_name)
  174. o_func = getattr(field, ordering)
  175. order_args.append(o_func())
  176. query = query.order_by(*order_args)
  177. return query
  178. @classmethod
  179. def _iterable_order_by(cls, iterable, order_by):
  180. """Sort iterable by field names in order_by
  181. :param iterable: model objects collection
  182. :param order_by: tuple of model fields names for sorting.
  183. If name starts with '-' desc ordering applies, else asc.
  184. """
  185. order_by_fields = []
  186. for field_name in order_by:
  187. if field_name.startswith('-'):
  188. order_by_fields.append({'name': field_name.lstrip('-'),
  189. 'lt': 1, 'gt': -1})
  190. else:
  191. order_by_fields.append({'name': field_name,
  192. 'lt': -1, 'gt': 1})
  193. # 'cmp' argument for 'sorted' function is removed in python3.
  194. # Next code should work fine for both python2 and python3
  195. key = functools.cmp_to_key(functools.partial(cls.single.compare,
  196. order_by=order_by_fields))
  197. return sorted(iterable, key=key)
  198. @classmethod
  199. def order_by(cls, iterable, order_by):
  200. """Order given iterable by specified order_by.
  201. :param iterable: model objects collection
  202. :param order_by: tuple of model fields names or single field name for
  203. ORDER BY criterion to SQLAlchemy query. If name starts with '-'
  204. desc ordering applies, else asc.
  205. :type order_by: tuple of strings or string
  206. """
  207. if iterable is None or not order_by:
  208. return iterable
  209. if not isinstance(order_by, (list, tuple)):
  210. order_by = (order_by,)
  211. if cls._is_query(iterable):
  212. return cls._query_order_by(iterable, order_by)
  213. else:
  214. return cls._iterable_order_by(iterable, order_by)
  215. @classmethod
  216. def filter_by(cls, iterable, **kwargs):
  217. """Filter given iterable by specified kwargs.
  218. In case if iterable=None filters all object instances
  219. :param iterable: iterable (SQLAlchemy query)
  220. :param order_by: tuple of model fields names for ORDER BY criterion
  221. to SQLAlchemy query. If name starts with '-' desc ordering applies,
  222. else asc.
  223. :returns: filtered iterable (SQLAlchemy query)
  224. """
  225. if iterable is not None:
  226. use_iterable = iterable
  227. else:
  228. use_iterable = cls.all()
  229. if cls._is_query(use_iterable):
  230. return use_iterable.filter_by(**kwargs)
  231. elif cls._is_iterable(use_iterable):
  232. return ifilter(
  233. lambda i: all(
  234. (getattr(i, k) == v for k, v in six.iteritems(kwargs))
  235. ),
  236. use_iterable
  237. )
  238. else:
  239. raise TypeError("First argument should be iterable")
  240. @classmethod
  241. def filter_by_not(cls, iterable, **kwargs):
  242. """Filter given iterable by specified kwargs with negation.
  243. In case if `iterable` is `None` filters all object instances.
  244. :param iterable: iterable (SQLAlchemy query)
  245. :returns: filtered iterable (SQLAlchemy query)
  246. """
  247. use_iterable = iterable or cls.all()
  248. if cls._is_query(use_iterable):
  249. conditions = []
  250. for key, value in six.iteritems(kwargs):
  251. conditions.append(
  252. getattr(cls.single.model, key) == value
  253. )
  254. return use_iterable.filter(not_(and_(*conditions)))
  255. elif cls._is_iterable(use_iterable):
  256. return ifilter(
  257. lambda i: not all(
  258. (getattr(i, k) == v for k, v in six.iteritems(kwargs))
  259. ),
  260. use_iterable
  261. )
  262. else:
  263. raise TypeError("First argument should be iterable")
  264. @classmethod
  265. def lock_for_update(cls, iterable):
  266. """Use SELECT FOR UPDATE on a given iterable (query).
  267. In case if iterable=None returns all object instances
  268. :param iterable: iterable (SQLAlchemy query)
  269. :returns: filtered iterable (SQLAlchemy query)
  270. """
  271. use_iterable = iterable or cls.all()
  272. if cls._is_query(use_iterable):
  273. return use_iterable.with_lockmode('update')
  274. elif cls._is_iterable(use_iterable):
  275. # we can't lock abstract iterable, so returning as is
  276. # for compatibility
  277. return use_iterable
  278. else:
  279. raise TypeError("First argument should be iterable")
  280. @classmethod
  281. def filter_by_list(cls, iterable, field_name, list_of_values,
  282. order_by=()):
  283. """Filter given iterable by list of list_of_values.
  284. In case if iterable=None filters all object instances
  285. :param iterable: iterable (SQLAlchemy query)
  286. :param field_name: filtering field name
  287. :param list_of_values: list of values for objects filtration
  288. :returns: filtered iterable (SQLAlchemy query)
  289. """
  290. field_getter = operator.attrgetter(field_name)
  291. use_iterable = iterable or cls.all()
  292. if cls._is_query(use_iterable):
  293. result = use_iterable.filter(
  294. field_getter(cls.single.model).in_(list_of_values)
  295. )
  296. result = cls.order_by(result, order_by)
  297. return result
  298. elif cls._is_iterable(use_iterable):
  299. return ifilter(
  300. lambda i: field_getter(i) in list_of_values,
  301. use_iterable
  302. )
  303. else:
  304. raise TypeError("First argument should be iterable")
  305. @classmethod
  306. def filter_by_id_list(cls, iterable, uid_list):
  307. """Filter given iterable by list of uids.
  308. In case if iterable=None filters all object instances
  309. :param iterable: iterable (SQLAlchemy query)
  310. :param uid_list: list of uids for objects
  311. :returns: filtered iterable (SQLAlchemy query)
  312. """
  313. return cls.filter_by_list(
  314. iterable,
  315. 'id',
  316. uid_list,
  317. )
  318. @classmethod
  319. def eager_base(cls, iterable, options):
  320. """Eager load linked object instances (SQLAlchemy FKs).
  321. In case if iterable=None applies to all object instances
  322. :param iterable: iterable (SQLAlchemy query)
  323. :param options: list of sqlalchemy eagerload types
  324. :returns: iterable (SQLAlchemy query)
  325. """
  326. use_iterable = iterable or cls.all()
  327. if options:
  328. return use_iterable.options(*options)
  329. return use_iterable
  330. @classmethod
  331. def eager(cls, iterable, fields):
  332. """Eager load linked object instances (SQLAlchemy FKs).
  333. By default joinedload will be applied to every field.
  334. If you want to use custom eagerload method - use eager_base
  335. In case if iterable=None applies to all object instances
  336. :param iterable: iterable (SQLAlchemy query)
  337. :param fields: list of links (model FKs) to eagerload
  338. :returns: iterable (SQLAlchemy query)
  339. """
  340. options = [joinedload(field) for field in fields]
  341. return cls.eager_base(iterable, options)
  342. @classmethod
  343. def count(cls, iterable=None):
  344. use_iterable = iterable or cls.all()
  345. if cls._is_query(use_iterable):
  346. return use_iterable.count()
  347. elif cls._is_iterable(use_iterable):
  348. return len(list(iterable))
  349. else:
  350. raise TypeError("First argument should be iterable")
  351. @classmethod
  352. def to_list(cls, iterable=None, fields=None, serializer=None):
  353. """Serialize iterable to list of dicts
  354. In case if iterable=None serializes all object instances
  355. :param iterable: iterable (SQLAlchemy query)
  356. :param fields: exact fields to serialize
  357. :param serializer: the custom serializer
  358. :returns: collection of objects as a list of dicts
  359. """
  360. use_iterable = cls.all() if iterable is None else iterable
  361. return [
  362. cls.single.to_dict(o, fields=fields, serializer=serializer)
  363. for o in use_iterable
  364. ]
  365. @classmethod
  366. def to_json(cls, iterable=None, fields=None):
  367. """Serialize iterable to JSON
  368. In case if iterable=None serializes all object instances
  369. :param iterable: iterable (SQLAlchemy query)
  370. :param fields: exact fields to serialize
  371. :returns: collection of objects as a JSON string
  372. """
  373. return jsonutils.dumps(
  374. cls.to_list(
  375. fields=fields,
  376. iterable=iterable
  377. )
  378. )
  379. @classmethod
  380. def create(cls, data):
  381. """Create object instance with specified parameters in DB
  382. :param data: dictionary of key-value pairs as object fields
  383. :returns: instance of an object (model)
  384. """
  385. return cls.single.create(data)
  386. @classmethod
  387. def options(cls, iterable, *args):
  388. """Apply the given list of mapper options.
  389. In case if iterable=None applies to all object instances
  390. :param iterable: iterable (SQLAlchemy query)
  391. :param options: list of sqlalchemy mapper options
  392. :returns: iterable (SQLAlchemy query)
  393. """
  394. use_iterable = iterable or cls.all()
  395. if args:
  396. return use_iterable.options(*args)
  397. return use_iterable