325 lines
10 KiB
Python
325 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2013 Mirantis, Inc.
|
|
#
|
|
# Licensed 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.
|
|
|
|
"""
|
|
Base classes for objects and collections
|
|
"""
|
|
|
|
import collections
|
|
import json
|
|
|
|
from itertools import ifilter
|
|
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from nailgun.objects.serializers.base import BasicSerializer
|
|
|
|
from nailgun.db import db
|
|
from nailgun.db import NoCacheQuery
|
|
from nailgun.errors import errors
|
|
|
|
|
|
class NailgunObject(object):
|
|
"""Base class for objects
|
|
"""
|
|
|
|
#: Serializer class for object
|
|
serializer = BasicSerializer
|
|
|
|
#: SQLAlchemy model for object
|
|
model = None
|
|
|
|
#: JSON schema for object
|
|
schema = {
|
|
"properties": {}
|
|
}
|
|
|
|
@classmethod
|
|
def check_field(cls, field):
|
|
"""Check if field is described in object's JSON schema
|
|
|
|
:param field: name of the field as string
|
|
:returns: None
|
|
:raises: errors.InvalidField
|
|
"""
|
|
if field not in cls.schema["properties"]:
|
|
raise errors.InvalidField(
|
|
u"Invalid field '{0}' for object '{1}'".format(
|
|
field,
|
|
cls.__name__
|
|
)
|
|
)
|
|
|
|
@classmethod
|
|
def get_by_uid(cls, uid, fail_if_not_found=False, lock_for_update=False):
|
|
"""Get instance by it's uid (PK in case of SQLAlchemy)
|
|
|
|
:param uid: uid of object
|
|
:param fail_if_not_found: raise an exception if object is not found
|
|
:param lock_for_update: lock returned object for update (DB mutex)
|
|
:returns: instance of an object (model)
|
|
"""
|
|
q = db().query(cls.model)
|
|
if lock_for_update:
|
|
q = q.with_lockmode('update')
|
|
res = q.get(uid)
|
|
if not res and fail_if_not_found:
|
|
raise errors.ObjectNotFound(
|
|
"Object '{0}' with UID={1} is not found in DB".format(
|
|
cls.__name__,
|
|
uid
|
|
)
|
|
)
|
|
return res
|
|
|
|
@classmethod
|
|
def create(cls, data):
|
|
"""Create object instance with specified parameters in DB
|
|
|
|
:param data: dictionary of key-value pairs as object fields
|
|
:returns: instance of an object (model)
|
|
"""
|
|
new_obj = cls.model()
|
|
for key, value in data.iteritems():
|
|
setattr(new_obj, key, value)
|
|
db().add(new_obj)
|
|
db().flush()
|
|
return new_obj
|
|
|
|
@classmethod
|
|
def update(cls, instance, data):
|
|
"""Update existing instance with specified parameters
|
|
|
|
:param instance: object (model) instance
|
|
:param data: dictionary of key-value pairs as object fields
|
|
:returns: instance of an object (model)
|
|
"""
|
|
instance.update(data)
|
|
db().add(instance)
|
|
db().flush()
|
|
return instance
|
|
|
|
@classmethod
|
|
def delete(cls, instance):
|
|
"""Delete object (model) instance
|
|
|
|
:param instance: object (model) instance
|
|
:returns: None
|
|
"""
|
|
db().delete(instance)
|
|
db().flush()
|
|
|
|
@classmethod
|
|
def save(cls, instance=None):
|
|
"""Save current changes for instance in DB.
|
|
Current transaction will be commited
|
|
(in case of SQLAlchemy).
|
|
|
|
:param instance: object (model) instance
|
|
:returns: None
|
|
"""
|
|
if instance:
|
|
db().add(instance)
|
|
db().commit()
|
|
|
|
@classmethod
|
|
def to_dict(cls, instance, fields=None):
|
|
"""Serialize instance to Python dict
|
|
|
|
:param instance: object (model) instance
|
|
:param fields: exact fields to serialize
|
|
:returns: serialized object (model) as dictionary
|
|
"""
|
|
return cls.serializer.serialize(instance, fields=fields)
|
|
|
|
@classmethod
|
|
def to_json(cls, instance, fields=None):
|
|
"""Serialize instance to JSON
|
|
|
|
:param instance: object (model) instance
|
|
:param fields: exact fields to serialize
|
|
:returns: serialized object (model) as JSON string
|
|
"""
|
|
return json.dumps(
|
|
cls.to_dict(instance, fields=fields)
|
|
)
|
|
|
|
|
|
class NailgunCollection(object):
|
|
"""Base class for object collections
|
|
"""
|
|
|
|
#: Single object class
|
|
single = NailgunObject
|
|
|
|
@classmethod
|
|
def _is_iterable(cls, obj):
|
|
return isinstance(
|
|
obj,
|
|
collections.Iterable
|
|
)
|
|
|
|
@classmethod
|
|
def _is_query(cls, obj):
|
|
return isinstance(
|
|
obj,
|
|
NoCacheQuery
|
|
)
|
|
|
|
@classmethod
|
|
def all(cls, yield_per=100):
|
|
"""Get all instances of this object (model)
|
|
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: iterable (SQLAlchemy query)
|
|
"""
|
|
return db().query(
|
|
cls.single.model
|
|
).yield_per(yield_per)
|
|
|
|
@classmethod
|
|
def filter_by(cls, iterable, yield_per=100, **kwargs):
|
|
"""Filter given iterable by specified kwargs.
|
|
In case if iterable=None filters all object instances
|
|
|
|
:param iterable: iterable (SQLAlchemy query)
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: filtered iterable (SQLAlchemy query)
|
|
"""
|
|
map(cls.single.check_field, kwargs.iterkeys())
|
|
use_iterable = iterable or cls.all(yield_per=yield_per)
|
|
if cls._is_query(use_iterable):
|
|
return use_iterable.filter_by(**kwargs)
|
|
elif cls._is_iterable(use_iterable):
|
|
return ifilter(
|
|
lambda i: all(
|
|
(getattr(i, k) == v for k, v in kwargs.iteritems())
|
|
),
|
|
use_iterable
|
|
)
|
|
else:
|
|
raise TypeError("First argument should be iterable")
|
|
|
|
@classmethod
|
|
def lock_for_update(cls, iterable, yield_per=100):
|
|
"""Use SELECT FOR UPDATE on a given iterable (query).
|
|
In case if iterable=None returns all object instances
|
|
|
|
:param iterable: iterable (SQLAlchemy query)
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: filtered iterable (SQLAlchemy query)
|
|
"""
|
|
use_iterable = iterable or cls.all(yield_per=yield_per)
|
|
if cls._is_query(use_iterable):
|
|
return use_iterable.with_lockmode('update')
|
|
elif cls._is_iterable(use_iterable):
|
|
# we can't lock abstract iterable, so returning as is
|
|
# for compatibility
|
|
return use_iterable
|
|
else:
|
|
raise TypeError("First argument should be iterable")
|
|
|
|
@classmethod
|
|
def get_by_id_list(cls, iterable, uid_list, yield_per=100):
|
|
"""Filter given iterable by list of uids.
|
|
In case if iterable=None filters all object instances
|
|
|
|
:param iterable: iterable (SQLAlchemy query)
|
|
:param uid_list: list of uids for objects
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: filtered iterable (SQLAlchemy query)
|
|
"""
|
|
use_iterable = iterable or cls.all(yield_per=yield_per)
|
|
if cls._is_query(use_iterable):
|
|
return use_iterable.filter(cls.single.model.id.in_(uid_list))
|
|
elif cls._is_iterable(use_iterable):
|
|
return ifilter(
|
|
lambda i: i.id in uid_list,
|
|
use_iterable
|
|
)
|
|
else:
|
|
raise TypeError("First argument should be iterable")
|
|
|
|
@classmethod
|
|
def eager(cls, iterable, fields, yield_per=100):
|
|
"""Eager load linked object instances (SQLAlchemy FKs).
|
|
In case if iterable=None applies to all object instances
|
|
|
|
:param iterable: iterable (SQLAlchemy query)
|
|
:param fields: list of links (model FKs) to eagerload
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: iterable (SQLAlchemy query)
|
|
"""
|
|
use_iterable = iterable or cls.all(yield_per=yield_per)
|
|
if fields:
|
|
return use_iterable.options(
|
|
*[joinedload(f) for f in fields]
|
|
)
|
|
return use_iterable
|
|
|
|
@classmethod
|
|
def count(cls, iterable=None):
|
|
use_iterable = iterable or cls.all()
|
|
if cls._is_query(use_iterable):
|
|
return use_iterable.count()
|
|
elif cls._is_iterable(use_iterable):
|
|
return len(list(iterable))
|
|
else:
|
|
raise TypeError("First argument should be iterable")
|
|
|
|
@classmethod
|
|
def to_list(cls, iterable=None, fields=None, yield_per=100):
|
|
"""Serialize iterable to list of dicts
|
|
In case if iterable=None serializes all object instances
|
|
|
|
:param iterable: iterable (SQLAlchemy query)
|
|
:param fields: exact fields to serialize
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: collection of objects as a list of dicts
|
|
"""
|
|
use_iterable = iterable or cls.all(yield_per=yield_per)
|
|
return map(
|
|
lambda o: cls.single.to_dict(o, fields=fields),
|
|
use_iterable
|
|
)
|
|
|
|
@classmethod
|
|
def to_json(cls, iterable=None, fields=None, yield_per=100):
|
|
"""Serialize iterable to JSON
|
|
In case if iterable=None serializes all object instances
|
|
|
|
:param iterable: iterable (SQLAlchemy query)
|
|
:param fields: exact fields to serialize
|
|
:param yield_per: SQLAlchemy's yield_per() clause
|
|
:returns: collection of objects as a JSON string
|
|
"""
|
|
return json.dumps(
|
|
cls.to_list(
|
|
fields=fields,
|
|
yield_per=yield_per,
|
|
iterable=iterable
|
|
)
|
|
)
|
|
|
|
@classmethod
|
|
def create(cls, data):
|
|
"""Create object instance with specified parameters in DB
|
|
|
|
:param data: dictionary of key-value pairs as object fields
|
|
:returns: instance of an object (model)
|
|
"""
|
|
return cls.single.create(data)
|