PKX MH4irm``passerine/__init__.pyfrom pkgutil import extend_path __path__ = extend_path(__path__, __name__) __version__ = (1, 0)PKX MHw$S S passerine/exception.py# Common / Unexpected Computation class UnexpectedComputationError(Exception): """ Exception used when the code runs mistakenly unexpectedly. """ # Common / Future exception class FutureFeatureException(Exception): """ Exception used when the future feature is used where it is not properly implemented. """ # Common / Invalid input class InvalidInput(Exception): """ Exception used when the given input is invalid or incompatible to the requirement. """ # Common / Object Dictionary class UnsupportObjectTypeError(Exception): """ Exception used when the unsupported object type is used in an inappropriate place. Please note that this is a general exception. """ # Decorator / Common / Singleton Decorator class SingletonInitializationException(Exception): """ This exception is used when the target class contain a special attribute `_singleton_instance` not a reference to its own class. """ # Dependency-injectable Application class InvalidConfigurationError(Exception): """ Exception thrown only when the configuration is invalid. """ # XML Configuration class DuplicatedPortError(Exception): """ Exception thrown only when the port config is duplicated within the same configuration file. """ # Routes class DuplicatedRouteError(Exception): """ Exception used when the routing pattern is already registered. """ class RoutingPatternNotFoundError(Exception): """ Exception used when the routing pattern is not specified in the configuration file. """ class RoutingTypeNotFoundError(Exception): """ Exception used when the routing type is not specified in the configuration file. """ class UnknownRoutingTypeError(Exception): """ Exception used when the routing type is not unknown. """ # Controller Directive class InvalidControllerDirectiveError(Exception): """ Exception used when the controller directive is incomplete due to missing parameter """ # Redirection Directive class InvalidRedirectionDirectiveError(Exception): """ Exception used when the redirection directive is incomplete because some parameters aren't provided or incompatible. """ # Fixtures class LoadedFixtureException(Exception): """ Exception raised when the fixture is loaded. """ # Session class SessionError(Exception): """ Exception thrown when there is an error with session component. """ # Template AbstractRepository and Rendering Service class RenderingSourceMissingError(Exception): """ Exception used when the rendering source is not set. """ class UnsupportedRendererError(Exception): """ Exception thrown when the unsupported renderer is being registered. """ class RendererSetupError(Exception): """ Exception thrown when there exists errors during setting up the template. """ class RendererNotFoundError(Exception): """ Exception thrown when the unknown template repository is used. """ # Services class UnknownServiceError(Exception): """ Exception thrown when the requested service is unknown or not found. """PKX MH+ + passerine/common.py# -*- coding: utf-8 -*- """ :Author: Juti Noppornpitak This package contains classes and functions for common use. """ import codecs import hashlib import logging import random from .decorator.common import singleton @singleton class LoggerFactory(object): def __init__(self, default_level=None): self.__default_level = default_level or logging.WARNING def set_default_level(self, default_level): self.__default_level = default_level return self def make(self, name, level=None, show_time=True): level = level or self.__default_level logging_handler = logging.StreamHandler() logging_handler.setLevel(level) logging_handler.setFormatter( logging.Formatter( '%(levelname)s %(asctime)s %(name)s: %(message)s' if show_time else '%(levelname)s %(name)s: %(message)s', datefmt='%Y.%m.%d %H:%M:%S %Z' ) ) logger = logging.getLogger(name) logger.addHandler(logging_handler) logger.setLevel(level) return logger def get_logger(name, level=None, show_time=True): return LoggerFactory.instance().make(name, level, show_time) @singleton class Enigma(object): """ Hashlib wrapper """ def hash(self, *data_list): """ Make a hash out of the given ``value``. :param `data_list`: the list of the data being hashed. :type `data_list`: list of string :return: the hashed data string """ hash_engine = hashlib.new('sha512') data_list = list(data_list) for i in range(len(data_list)): data_list[i] = codecs.encode(data_list[i], 'ascii') hash_engine.update(b''.join(data_list)) return hash_engine.hexdigest() def random_number(self): return random.randint(0,100000000000) class Finder(object): """ File System API Wrapper """ def read(self, file_path, is_binary=False): """ Read a file from *file_path*. By default, read a file normally. If *is_binary* is ``True``, the method will read in binary mode. """ with codecs.open(file_path, 'rb' if is_binary else 'r', 'utf-8') as fp: file_content = fp.read() fp.close() return file_content PKX MH%\ܫpasserine/graph.pyfrom time import time class DependencyNode(object): """ Dependency Node This is designed to be bi-directional to maximize flexibility on traversing the graph. """ def __init__(self): self.created_at = int(time() * 1000000) self.adjacent_nodes = set() self.reverse_edges = set() self.walked = False def connect(self, other): self.adjacent_nodes.add(other) other.reverse_edges.add(self) def _disavow_connection(self, node): return False @property def score(self): score = 0 for node in self.reverse_edges: if self._disavow_connection(node): continue score += 1 return score def __eq__(self, other): return self.created_at == other.created_at def __ne__(self, other): return self.created_at != other.created_at def __lt__(self, other): return self.score < other.score def __le__(self, other): return self.score <= other.score def __gt__(self, other): return self.score > other.score def __ge__(self, other): return self.score >= other.score def __hash__(self): return self.created_at def __repr__(self): return ''.format(self.object_id, self.score) class DependencyManager(object): @staticmethod def get_order(dependency_map): # After constructing the dependency graph (as a supposedly directed acyclic # graph), do the topological sorting from the dependency graph. final_order = [] for id in dependency_map: node = dependency_map[id] DependencyManager._retrieve_dependency_order(node, final_order) return final_order @staticmethod def _retrieve_dependency_order(node, priority_order): if node.walked: return node.walked = True initial_order = list(node.adjacent_nodes) for adjacent_node in initial_order: DependencyManager._retrieve_dependency_order(adjacent_node, priority_order) if node not in priority_order: priority_order.append(node)PKX MH[ۘ66passerine/db/session.py import re from passerine.db.common import ProxyObject, ProxyFactory, ProxyCollection from passerine.db.repository import Repository from passerine.db.entity import get_relational_map from passerine.db.exception import IntegrityConstraintError, UnsupportedRepositoryReferenceError from passerine.db.mapper import AssociationType from passerine.db.metadata.entity import EntityMetadata from passerine.db.metadata.helper import EntityMetadataHelper from passerine.db.uow import UnitOfWork from passerine.graph import DependencyNode, DependencyManager class QueryIteration(DependencyNode): def __init__(self, join_config, alias, parent_alias, property_path): super(QueryIteration, self).__init__() self._join_config = join_config self._alias = alias self._parent_alias = parent_alias self._property_path = property_path @property def join_config(self): return self._join_config @property def alias(self): return self._alias @property def parent_alias(self): return self._parent_alias @property def property_path(self): return self._property_path def to_dict(self): return { 'property_path': self.property_path, 'parent_alias': self.parent_alias, 'alias': self.alias, 'join_config': self.join_config, 'adjacent_nodes':self.adjacent_nodes } def __repr__(self): return str('{}({})'.format(self.__class__.__name__, self.to_dict())) class Session(object): """ Database Session :param database_name: the database name :param driver: the driver API """ def __init__(self, driver): self._driver = driver self._uow = UnitOfWork(self) self._repository_map = {} self._registered_types = {} self._re_property_path_delimiter = re.compile('\.') @property def driver(self): return self._driver def collection(self, entity_class): """ Alias to ``repository()`` .. deprecated:: 2.2 """ return self.repository(entity_class) def repositories(self): """ Retrieve the list of collections :rtype: list """ return [self._repository_map[key] for key in self._repository_map] def repository(self, reference): """ Retrieve the collection :param reference: the entity class or entity metadata of the target repository / collection :rtype: passerine.db.repository.Repository """ key = None if isinstance(reference, EntityMetadata): key = reference.collection_name elif EntityMetadataHelper.hasMetadata(reference): is_registerable_reference = True metadata = EntityMetadataHelper.extract(reference) key = metadata.collection_name self.register_class(reference) if not key: raise UnsupportedRepositoryReferenceError('Either a class with metadata or an entity metadata is supported.') if key not in self._repository_map: repository = Repository( session = self, representing_class = reference ) repository.setup_index() self._repository_map[key] = repository return self._repository_map[key] def register_class(self, entity_class): """ Register the entity class :param type entity_class: the class of document/entity :rtype: passerine.db.repository.Repository .. note:: This is for internal operation only. As it seems to be just a residual from the prototype stage, the follow-up investigation in order to remove the method will be for Tori 3.1. """ key = entity_class if isinstance(entity_class, type): metadata = EntityMetadataHelper.extract(entity_class) key = metadata.collection_name if key not in self._registered_types: self._registered_types[key] = entity_class def query(self, query): """ Query the data :param passerine.db.query.Query query: the query object :return: the list of matched entities :rtype: list """ metadata = EntityMetadataHelper.extract(query.origin) # Deprecated in Tori 3.1; Only for backward compatibility if not query.is_new_style: return self.driver.query( metadata, query._condition, self.driver.dialect.get_iterating_constrains(query) ) root_class = query.origin expression_set = query.criteria.get_analyzed_version() # Register the root entity query.join_map[query.alias] = { 'alias': query.alias, 'path': None, 'class': root_class, 'parent_alias': None, 'property_path': None, 'result_list': [] } self._update_join_map(metadata, query.join_map, query.alias) iterating_sequence = self._compute_iterating_sequence(query.join_map) alias_to_query_map = self.driver.dialect.get_alias_to_native_query_map(query) for iteration in iterating_sequence: if not self._sub_query(query, alias_to_query_map, iteration): break return query.join_map[query.alias]['result_list'] def _sub_query(self, query, alias_to_query_map, iteration): is_join_query = True alias = iteration.alias if alias not in alias_to_query_map: return False join_config = query.join_map[alias] joined_type = join_config['class'] joined_meta = EntityMetadataHelper.extract(joined_type) native_query = alias_to_query_map[alias] local_constrains = {} if not iteration.parent_alias: is_root = False constrains = self.driver.dialect.get_iterating_constrains(query) result_list = self.driver.query(joined_meta, native_query, local_constrains) # No result in a sub-query means no result in the main query. if not result_list: return False join_config['result_list'] = result_list alias_to_query_map.update(self.driver.dialect.get_alias_to_native_query_map(query)) return True def _compute_iterating_sequence(self, join_map): iterating_sequence = [] joining_sequence = [] reference_map = {} # reference_map is used locally for fast reverse lookup # iterating_seq is a final sequence # Calculate the iterating sequence for alias in join_map: join_config = join_map[alias] parent_alias = None property_path = None if join_config['path']: parent_alias, property_path = join_config['path'].split('.', 2) qi = QueryIteration(join_config, alias, parent_alias, property_path) joining_sequence.append(qi) reference_map[alias] = qi # Update the dependency map for key in reference_map: reference_a = reference_map[key] if reference_a.parent_alias not in reference_map: continue reference_a.connect(reference_map[reference_a.parent_alias]) iterating_sequence = DependencyManager.get_order(reference_map) iterating_sequence.reverse() return iterating_sequence def _update_join_map(self, origin_metadata, join_map, origin_alias): link_map = origin_metadata.relational_map iterating_sequence = [] # Compute the (local) iterating sequence for updating the join map. # Note: this is not the query iterating sequence. for alias in join_map: join_config = join_map[alias] if join_config['class']: continue parent_alias, property_path = join_config['path'].split('.', 2) join_config['alias'] = alias join_config['property_path'] = property_path join_config['parent_alias'] = parent_alias join_config['result_list'] = [] iterating_sequence.append((join_config, alias, parent_alias, property_path)) # Update the immediate properties. for join_config, current_alias, parent_alias, property_path in iterating_sequence: if parent_alias != origin_alias: continue if property_path not in link_map: continue mapper = link_map[property_path] join_config['class'] = mapper.target_class join_config['mapper'] = mapper # Update the joined properties. for join_config, current_alias, parent_alias, property_path in iterating_sequence: if current_alias not in join_map: continue if not join_map[current_alias]['class']: continue next_origin_class = join_map[current_alias]['class'] next_metadata = EntityMetadataHelper.extract(next_origin_class) self._update_join_map(next_metadata, join_map, current_alias) def delete(self, *entities): """ Delete entities :param entities: one or more entities :type entities: type of list of type """ for entity in entities: targeted_entity = self._force_load(entity) self._uow.register_deleted(targeted_entity) def refresh(self, *entities): """ Refresh entities :param entities: one or more entities :type entities: type of list of type """ for entity in entities: self.refresh_one(entity) def refresh_one(self, entity): self._uow.refresh(self._force_load(entity)) def persist(self, *entities): """ Persist entities :param entities: one or more entities :type entities: type of list of type """ for entity in entities: self.persist_one(entity) def persist_one(self, entity): targeted_entity = self._force_load(entity) registering_action = self._uow.register_new \ if self._uow.is_new(targeted_entity) \ else self._uow.register_dirty registering_action(targeted_entity) def recognize(self, entity): self._uow.register_clean(self._force_load(entity)) def flush(self): """ Flush all changes of the session. """ self._uow.commit() def find_record(self, id, cls): return self._uow.find_recorded_entity(id, cls) def apply_relational_map(self, entity): """ Wire connections according to the relational map """ meta = EntityMetadataHelper.extract(entity) rmap = meta.relational_map for property_name in rmap: guide = rmap[property_name] """ :type: passerine.db.mapper.RelatingGuide """ # In the reverse mapping, the lazy loading is not possible but so # the proxy object is still used. if guide.inverted_by: target_meta = EntityMetadataHelper.extract(guide.target_class) api = self._driver.collection(target_meta.collection_name) if guide.association in [AssociationType.ONE_TO_ONE, AssociationType.MANY_TO_ONE]: # Replace with Criteria target = api.find_one({guide.inverted_by: entity.id}) entity.__setattr__(property_name, ProxyFactory.make(self, target['_id'], guide)) elif guide.association == AssociationType.ONE_TO_MANY: # Replace with Criteria proxy_list = [ ProxyFactory.make(self, target['_id'], guide) for target in api.find({guide.inverted_by: entity.id}) ] entity.__setattr__(property_name, proxy_list) elif guide.association == AssociationType.MANY_TO_MANY: entity.__setattr__(property_name, ProxyCollection(self, entity, guide)) else: raise IntegrityConstraintError('Unknown type of entity association (reverse mapping)') return # Done the application # In the direct mapping, the lazy loading is applied wherever applicable. if guide.association in [AssociationType.ONE_TO_ONE, AssociationType.MANY_TO_ONE]: if not entity.__getattribute__(property_name): continue entity.__setattr__( property_name, ProxyFactory.make( self, entity.__getattribute__(property_name), guide ) ) elif guide.association == AssociationType.ONE_TO_MANY: proxy_list = [] for object_id in entity.__getattribute__(property_name): if not object_id: continue proxy_list.append(ProxyFactory.make(self, object_id, guide)) entity.__setattr__(property_name, proxy_list) elif guide.association == AssociationType.MANY_TO_MANY: entity.__setattr__(property_name, ProxyCollection(self, entity, guide)) else: raise IntegrityConstraintError('Unknown type of entity association') def _force_load(self, entity): return entity._actual \ if isinstance(entity, ProxyObject) \ else entityPKX MHvypasserine/db/query.py""" :mod:`passerine.db.criteria` -- Query Criteria ========================================= .. module:: passerine.db.criteria :platform: All :synopsis: The metadata for querying data from the backend data storage .. moduleauthor:: Juti Noppornpitak """ import pymongo from imagination.decorator.validator import restrict_type from passerine.db.expression import Criteria class Order(object): """ Sorting Order Definition """ ASC = pymongo.ASCENDING """ Ascending Order """ DESC = pymongo.DESCENDING """ Descending Order """ class Query(object): """ Criteria .. note:: The current implementation does not support filtering on associated entities. """ def __init__(self, alias): self._alias = alias self._condition = {} self._origin = None # str - the name of the collection / repository self._order_by = [] self._offset = 0 self._limit = 0 self._indexed = False self._force_loading = False self._auto_index = False self._indexed_target_list = [] self._criteria = None self._join_map = {} self._definition_map = {} @property def is_new_style(self): return bool(self.criteria) @property def definition_map(self): return self._definition_map @definition_map.setter def definition_map(self, value): self._definition_map = value @property def alias(self): return self._alias @alias.setter def alias(self, value): self._alias = value @property def origin(self): return self._origin @origin.setter def origin(self, value): self._origin = value @property def criteria(self): """ Expression Criteria """ return self._criteria @criteria.setter def criteria(self, value): expected_type = Criteria if not isinstance(value, expected_type): raise ValueError('The {} object should be given, not {}.'.format(expected_type.__name__, type(value).__name__)) self._criteria = value @property def join_map(self): """ A join map """ return self._join_map @join_map.setter def join_map(self, value): self._join_map = value def join(self, property_path, alias): """ Define a join path """ if not (alias != self.alias and alias not in self._join_map): raise KeyError('The alias of the joined entity must be unique.') self._join_map[alias] = { 'path': property_path, 'class': None, 'mapper': None } return self def order(self, field, direction=Order.ASC): """ Define the returning order :param field: the sorting field :type field: str :param direction: the sorting direction """ if field == 'id': field = '_id' self._order_by.append((field, direction)) return self def start(self, offset): """ Define the filter offset :param offset: the filter offset :type offset: int """ self._offset = offset return self def limit(self, limit): """ Define the filter limit :param limit: the filter limit :type limit: int """ self._limit = limit return self def force_loading(self, flag): self._force_loading = flag return self def auto_index(self, flag): self._auto_index = flag return self def new_criteria(self): """ Get a new expression for this criteria :rtype: passerine.db.expression.Criteria """ return Criteria() def expect(self, statement): """ Define the condition / expectation of the main expression. :param statement: the conditional statement :type statement: str This is a shortcut expression to define expectation of the main expression. The main expression will be defined automatically if it is undefined. For example, .. code-block:: python c = Query() c.expect('foo = 123') is the same thing as .. code-block:: python c = Query() c.criteria = c.new_criteria() c.criteria.expect('foo = 123') """ if not self.criteria: self.criteria = self.new_criteria() self.criteria.expect(statement) return self def define(self, variable_name=None, value=None, **definition_map): """ Define the value of one or more variables (known as parameters). :param variable_name: the name of the variable (for single assignment) :type variable_name: str :param value: the value of the variable (for single assignment) :param definition_map: the variable-to-value dictionary This method is usually recommended be used to define multiple variables like the following example. .. code-block:: python criteria.define(foo = 'foo', bar = 2) However, it is designed to support the assign of a single user. For instance, .. code-block:: python criteria.define('foo', 'foo').define('bar', 2) """ is_single_definition = bool(variable_name and value) is_batch_definition = bool(definition_map) if is_single_definition and not is_batch_definition: self.definition_map[variable_name] = value return self elif not is_single_definition and is_batch_definition: self.definition_map.update(definition_map) return self raise ValueError('Cannot define one variable or multiple variables at the same time.') def reset_definitions(self): self.definition_map = {} def __str__(self): statements = [] if self._condition: statements.append('WHERE ' + str(self._condition)) if self._order_by: statements.append('ORDER BY ' + str(self._order_by)) if self._offset: statements.append('OFFSET ' + str(self._offset)) if self._limit: statements.append('LIMIT ' + str(self._limit)) return ' '.join(statements)PKX MHpasserine/db/__init__.pyPKX MHS* % %passerine/db/repository.py# -*- coding: utf-8 -*- """ :Author: Juti Noppornpitak :Status: Stable """ import inspect from passerine.db.common import PseudoObjectId, ProxyObject from passerine.db.query import Query, Order from passerine.db.exception import MissingObjectIdException, EntityAlreadyRecognized, EntityNotRecognized from passerine.db.mapper import AssociationType, CascadingType from passerine.db.uow import Record from passerine.db.metadata.helper import EntityMetadataHelper class Repository(object): """ Repository (Entity AbstractRepository) for Mongo DB :param session: the entity manager :type session: passerine.db.session.Session :param representing_class: the representing class :type representing_class: type A repository may automatically attempt to create an index if :meth:`auto_index` define the auto-index flag. Please note that the auto-index feature is only invoked when it tries to use a criteria with sorting or filtering with a certain type of conditions. """ def __init__(self, session, representing_class): self._class = representing_class self._session = session self._has_cascading = None self._auto_index = False # Retrieve the collection self._session.register_class(representing_class) @property def session(self): """ Session :rtype: passerine.db.session.Session """ return self._session @property def driver(self): return self._session.driver @property def name(self): """ Collection name :rtype: str """ metadata = EntityMetadataHelper.extract(self._class) return metadata.collection_name @property def kind(self): return self._class def auto_index(self, auto_index): """ Enable the auto-index feature :param auto_index: the index flag :type auto_index: bool """ self._auto_index = auto_index def new(self, **attributes): """ Create a new document/entity :param attributes: attribute map :return: object .. note:: This method deal with data mapping. """ spec = inspect.getargspec(self._class.__init__) # constructor contract meta = EntityMetadataHelper.extract(self._class) rmap = meta.relational_map # relational map # Default missing argument to NULL or LIST # todo: respect the default value of the argument for argument_name in spec.args: if argument_name == 'self' or argument_name in attributes: continue default_to_list = argument_name in rmap\ and rmap[argument_name].association in [ AssociationType.ONE_TO_MANY, AssociationType.MANY_TO_MANY ] attributes[argument_name] = [] if default_to_list else None attribute_name_list = list(attributes.keys()) # Remove unwanted arguments/attributes/properties for attribute_name in attribute_name_list: if argument_name == 'self' or attribute_name in spec.args: continue del attributes[attribute_name] return self._class(**attributes) def get(self, id): data = self._session.driver.find_one(self.name, {'_id': id}) if not data: return None return self._dehydrate_object(data) def find(self, criteria=None, force_loading=False): """ Find entity with criteria :param criteria: the search criteria :type criteria: passerine.db.criteria.Query :param force_loading: the flag to force loading all references behind the proxy :type force_loading: bool :returns: the result based on the given criteria :rtype: object or list of objects """ if not criteria: criteria = self.new_criteria() data_set = self.session.query(criteria) entity_list = [] for data in data_set: entity = self._dehydrate_object(data) \ if len(data.keys()) > 1 \ else ProxyObject( self._session, self._class, data['_id'], False, None, False ) record = self._session.find_record(id, self._class) if record and record.status in [Record.STATUS_DELETED, Record.STATUS_IGNORED]: continue entity_list.append(entity) if criteria._limit == 1: return entity_list[0] if entity_list else None return entity_list def count(self, criteria): """ Count the number of entities satisfied the given criteria :param criteria: the search criteria :type criteria: passerine.db.criteria.Query :rtype: int """ return criteria.build_cursor(self).count() def filter(self, condition={}, force_loading=False): """ Shortcut method for :method:`find`. """ criteria = self.new_criteria() criteria._force_loading = force_loading for k in condition: criteria.expect('e.{key} = :{key}'.format(key = k)) criteria.define(k, condition[k]) return self.find(criteria) def filter_one(self, condition={}, force_loading=False): """ Shortcut method for :method:`find`. """ criteria = self.new_criteria() criteria._force_loading = force_loading for k in condition: criteria.expect('e.{key} = :{key}'.format(key = k)) criteria.define(k, condition[k]) criteria.limit(1) return self.find(criteria) def post(self, entity): if entity.__session__: raise EntityAlreadyRecognized('The entity has already been recognized by this session.') self._session.persist(entity) entity.__session__ = self._session self.commit() return entity.id def put(self, entity): self._recognize_entity(entity) self._session.persist(entity) self.commit() def delete(self, entity): self._recognize_entity(entity) self._session.delete(entity) self.commit() def persist(self, entity): self._session.persist(entity) def commit(self): self._session.flush() def _recognize_entity(self, entity): if not entity or not entity.id or not entity.__session__ or isinstance(entity.id, PseudoObjectId): raise EntityNotRecognized('The entity is not recognized by this session.') def _dehydrate_object(self, raw_data): if '_id' not in raw_data: raise MissingObjectIdException('The key _id in the raw data is not found.') id = raw_data['_id'] record = self._session.find_record(id, self._class) # Returned the known document from the record. if record: return record.entity data = dict(raw_data) del data['_id'] document = self.new(**data) document.id = id document.__session__ = self._session self._session.apply_relational_map(document) self._session.recognize(document) return document def has_cascading(self): if self._has_cascading is not None: return self._has_cascading self._has_cascading = False relational_map = EntityMetadataHelper.extract(self._class).relational_map for property_name in relational_map: cascading_options = relational_map[property_name].cascading_options if cascading_options \ and ( CascadingType.DELETE in cascading_options \ or CascadingType.PERSIST in cascading_options ): self._has_cascading = True break return self._has_cascading def new_criteria(self, alias='e'): """ Create a criteria :rtype: :class:`passerine.db.criteria.Query` """ c = Query(alias) c.origin = self._class if self._auto_index: c.auto_index(self._auto_index) return c def index(self, index, force_index=False): """ Index data :param index: the index :type index: list, passerine.db.entity.Index or str :param force_index: force indexing if necessary :type force_index: bool """ self._session.driver.ensure_index(self.name, index, force_index) def setup_index(self): """ Set up index for the entity based on the ``entity`` and ``link`` decorators """ metadata = EntityMetadataHelper.extract(self._class) # Apply the relational indexes. for field in metadata.relational_map: guide = metadata.relational_map[field] if guide.inverted_by or guide.association != AssociationType.ONE_TO_ONE: continue self.index(field) # Apply the manual indexes. for index in metadata.index_list: self.index(index) def __len__(self): return self._session.driver.total_row_count(self.name)PK"MHQKUpasserine/db/manager.pyfrom contextlib import contextmanager import re from bson.objectid import ObjectId from imagination.loader import Loader from imagination.decorator.validator import restrict_type from passerine.db.driver.interface import DriverInterface from passerine.db.session import Session from passerine.db.exception import InvalidUrlError, UnknownDriverError class ManagerFactory(object): """ Manager Factory :param dict urls: the alias-to-endpoint-URL map :param dict protocols: the protocol-to-fully-qualified-module-path map """ def __init__(self, urls = None, protocols = None): self._protocol_to_driver_map = {} self._alias_to_manager_map = {} # Clone the map to ensure that the map cannot be tempered easily from the external code. self._alias_to_url_map = dict(urls or {}) self._re_url = re.compile('(?P[a-zA-Z]+)://(?P
.+)') p2d_map = (protocols or self._default_protocol_to_driver_map) for protocol in p2d_map: self.register(protocol, p2d_map[protocol]) @property def _default_protocol_to_driver_map(self): return { 'mongodb': 'passerine.db.driver.mongodriver.Driver', #'riak': 'passerine.db.driver.riakdriver.Driver', } def register(self, protocol, driver_class): """ Register the protocol to the driver class. :param str protocol: the protocol string (e.g., mongodb, riak) :param passerine.db.driver.interface.DriverInterface driver_class: a DriverInterface-based class (type) """ self._protocol_to_driver_map[protocol] = driver_class \ if isinstance(driver_class, type) \ else Loader(driver_class).package def analyze_url(self, url): index = 0 connection_info = {} matches = self._re_url.match(url) if matches: return matches.groupdict() raise InvalidUrlError('Invalid URL to {}'.format(url)) def driver(self, url): config = self.analyze_url(url) if not config: raise UnknownDriverError('Unable to connect to {} due to invalid configuration'.format()) if config['protocol'] in self._protocol_to_driver_map: return self._protocol_to_driver_map[config['protocol']](url) raise UnknownDriverError('Unable to connect to {}'.format(url)) def set(self, alias, url): self._alias_to_url_map[alias] = url def get(self, alias): return self.connect(self._alias_to_url_map[alias], alias) def connect(self, url, alias = None): if alias in self._alias_to_manager_map: return self._alias_to_manager_map[alias] driver = self.driver(url) manager = Manager(driver) if alias: self._alias_to_manager_map[alias] = manager return manager class Manager(object): """ Entity Manager :param driver: the driver interface :type driver: passerine.db.driver.interface.DriverInterface """ def __init__(self, driver): assert isinstance(driver, DriverInterface) or issubclass(driver, DriverInterface), \ 'The given driver must implement DriverInterface, {} given.'.format(driver) self._driver = driver self._session_map = {} self._driver = driver @property def driver(self): """ Driver API :rtype: passerine.db.driver.interface.DriverInterface """ return self._driver @contextmanager def session(self, id=None, supervised=False): """ Open a session in the context manager. :param id: the session ID :param bool supervised: the flag to indicate that the opening session will be observed and supervised by the manager. This allows the session to be reused by multiple components. However, it is not **thread-safe**. It is disabled by default. .. note:: The end of the context will close the open session. """ session = self.open_session(id, supervised) yield session if id: self.close_session(id) return del session def open_session(self, id=None, supervised=False): """ Open a session :param id: the session ID :param bool supervised: the flag to indicate that the opening session will be observed and supervised by the manager. This allows the session to be reused by multiple components. However, it is not **thread-safe**. It is disabled by default. """ if not supervised: return Session(self.driver) if not id: id = ObjectId() if id in self._session_map: return self._session_map[id] session = Session(self.driver) if supervised: self._session_map[id] = session return session def close_session(self, id): """ Close the managed session .. warning:: This method is designed to bypass errors when the given ID is unavailable or already closed. """ if id not in self._session_map: return del self._session_map[id] def get_repository(self, ref_class, session_id=None): """ Retrieve the repository. :param type ref_class: The reference class :param session_id: The supervised session ID :rtype: passerine.db.repository.Repository .. note:: This is designed for Imagination's factorization. .. note:: With an unsupervised session, the memory usage may be higher than usual as the memory reference may not be freed as long as the reference to the returned repository continues to exist in active threads. """ loose_session = self.open_session(session_id, bool(session_id)) return loose_session.repository(ref_class) PKX MH#pq)q)passerine/db/mapper.py# -*- coding: utf-8 -*- """ .. note:: The current implementation doesn't support merging or detaching a document simultaneously observed by at least two entity manager. """ import hashlib from imagination.loader import Loader from passerine.db.exception import DuplicatedRelationalMapping from passerine.db.metadata.helper import EntityMetadataHelper class AssociationType(object): """ Association Type """ AUTO_DETECT = 1 # Not supported in the near future """ Auto detection (default, disabled and raising exception) """ ONE_TO_ONE = 2 """ One-to-one association mode """ ONE_TO_MANY = 3 """ One-to-many association mode """ MANY_TO_ONE = 4 """ Many-to-one association mode """ MANY_TO_MANY = 5 """ Many-to-many association mode """ @staticmethod def known_type(t): """ Check if it is a known type :param t: type :type t: int :returns: ``True`` if it is a known type. :rtype: bool """ return AssociationType.AUTO_DETECT <= t <= AssociationType.MANY_TO_MANY class CascadingType(object): """ Cascading Type """ PERSIST = 1 """ Cascade on persist operation """ DELETE = 2 """ Cascade on delete operation """ MERGE = 3 """ Cascade on merge operation .. note:: Supported in Tori 2.2 """ DETACH = 4 # Supported in Tori 2.2 """ Cascade on detach operation .. note:: Supported in Tori 2.2 """ REFRESH = 5 """ Cascade on refresh operation """ class AssociationFactory(object): """ Association Factory """ class_name_tmpl = '{origin_module}{origin}{destination_module}{destination}' collection_name_tmpl = '{origin}_{destination}' code_template = '\n'.join([ 'from passerine.db.entity import BasicAssociation, entity', '@entity("{collection_name}")', 'class {class_name}(BasicAssociation): pass' ]) def __init__(self, origin, guide, cascading_options, is_reverse_mapping): self.__is_reverse_mapping = is_reverse_mapping self.__origin = origin self.__guide = guide self.__destination = None self.__cascading_options = cascading_options self.__class = None self.__class_name = None self.__collection_name = None if is_reverse_mapping: self.__origin = None self.__destination = origin self.__cascading_options = [] @property def origin(self): """ Origin :rtype: type """ if not self.__origin: self.__origin = self.__guide.target_class return self.__origin @property def destination(self): """ Destination :rtype: type """ if not self.__destination: self.__destination = self.__guide.target_class return self.__destination @property def class_name(self): """ Auto-generated Association Class Name :rtype: str .. note:: This is a read-only property. """ if not self.__class_name: self.__class_name = self.hash_content(self.class_name_tmpl.format( origin_module = self.origin.__module__, destination_module = self.destination.__module__, origin = self.origin.__name__, destination = self.destination.__name__ )) self.__class_name = 'Association{}'.format(self.__class_name) return self.__class_name @property def collection_name(self): """ Auto-generated Collection Name :rtype: str .. note:: This is a read-only property. """ if not self.__collection_name: self.__collection_name = self.collection_name_tmpl.format( origin = EntityMetadataHelper.extract(self.origin).collection_name, destination = EntityMetadataHelper.extract(self.destination).collection_name ) return self.__collection_name @property def cls(self): """ Auto-generated Association Class :rtype: type .. note:: This is a read-only property. """ if not self.__class: source = self.code_template.format( collection_name = self.collection_name, class_name = self.class_name ) code = compile(source, '', 'exec') exec(code, globals()) if self.class_name in globals(): self.__class = globals()[self.class_name] else: raise RuntimeError('Unable to auto-generation associative collection class.') return self.__class def hash_content(self, content): return hashlib.sha224(content.encode('ascii')).hexdigest() class BasicGuide(object): """ Basic Relation Guide This class is abstract and used with the relational map of the given entity class. :param target_class: the target class or class name (e.g., acme.entity.User) :type target_class: object :param association: the type of association :type association: int """ def __init__(self, target_class, association): self._target_class = target_class self.association = association @property def target_class(self): """ The target class :rtype: type """ if isinstance(self._target_class, Loader): self._target_class = self._target_class.package return self._target_class class RelatingGuide(BasicGuide): """ Relation Guide This class is used with the relational map of the given entity class. :param entity_class: the reference of the current class :type entity_class: type :param mapped_by: the name of property of the current class :type mapped_by: str :param target_class: the target class or class name (e.g., acme.entity.User) :type target_class: type :param inverted_by: the name of property of the target class :type inverted_by: str :param association: the type of association :type association: int :param read_only: the flag to indicate whether this is for read only. :type read_only: bool :param cascading_options: the list of actions on cascading :type cascading_options: list or tuple """ def __init__(self, entity_class, target_class, inverted_by, association, read_only, cascading_options): BasicGuide.__init__(self, target_class, association) self.origin_class = entity_class self.inverted_by = inverted_by self.read_only = read_only self.cascading_options = cascading_options # This is only used for many-to-many association. self.association_class = AssociationFactory( entity_class, self, cascading_options, self.inverted_by != None )\ if association == AssociationType.MANY_TO_MANY\ else None def __prevent_duplicated_mapping(cls, property_name): if not cls: raise ValueError('Expecting a valid type') metadata = EntityMetadataHelper.extract(cls) if property_name in metadata.relational_map: raise DuplicatedRelationalMapping('The property is already mapped.') def __map_property(cls, property_name, guide): metadata = EntityMetadataHelper.extract(cls) metadata.relational_map[property_name] = guide def map(cls, mapped_by=None, target=None, inverted_by=None, association=AssociationType.AUTO_DETECT, read_only=False, cascading=[]): """ Map the given class property to the target class. .. versionadded:: 2.1 :param cls: the reference of the current class :type cls: type :param mapped_by: the name of property of the current class :type mapped_by: str :param target: the target class or class name (e.g., acme.entity.User) :type target: type :param inverted_by: the name of property of the target class :type inverted_by: str :param association: the type of association :type association: int :param read_only: the flag to indicate whether this is for read only. :type read_only: bool :param cascading: the list of actions on cascading :type cascading: list or tuple """ if association == AssociationType.AUTO_DETECT: raise ValueError('The association is not specified.') if not AssociationType.known_type(association): raise ValueError('Unknown association') # Allow a name of classes as a target (e.g., acme.entity.User or 'acme.entity.User') if isinstance(target, str): loader = Loader(target) target = loader __prevent_duplicated_mapping(cls, mapped_by) __map_property( cls, mapped_by, RelatingGuide( cls, target or cls, inverted_by, association, read_only, cascading ) ) def link(mapped_by=None, target=None, inverted_by=None, association=AssociationType.AUTO_DETECT, read_only=False, cascading=[]): """ Association decorator .. versionadded:: 2.1 This is to map a property of the current class to the target class. :param mapped_by: the name of property of the current class :type mapped_by: str :param target: the target class or class name (e.g., acme.entity.User) :type target: type :param inverted_by: the name of property of the target class :type inverted_by: str :param association: the type of association :type association: int :param read_only: the flag to indicate whether this is for read only. :type read_only: bool :param cascading: the list of actions on cascading :type cascading: list or tuple :return: the decorated class :rtype: type .. tip:: If ``target`` is not defined, the default target will be the reference class. """ def decorator(cls): map(cls, mapped_by, target, inverted_by, association, read_only, cascading) return cls return decoratorPKX MHPPpasserine/db/exception.pyclass UnsupportedRepositoryReferenceError(Exception): """ Unsupported Repository Reference Error """ class UnknownDriverError(Exception): """ Unknown Driver Error """ class InvalidUrlError(Exception): """ Invalid DB URL Error""" class DuplicatedRelationalMapping(Exception): """ Exception thrown when the property is already mapped. """ class UnavailableCollectionException(Exception): """ Exception thrown when the collection is not available. """ class LockedIdException(Exception): """ Exception thrown when the ID is tempted to change. """ class MissingObjectIdException(Exception): """ Exception raised when the object Id is not specified during data retrieval. """ class UOWRepeatedRegistrationError(IOError): """ Error thrown when the given reference is already registered as a new reference or already existed. """ class UOWUnknownRecordError(IOError): """ Error thrown when the given reference is already registered as a new reference or already existed. """ class UOWUpdateError(IOError): """ Error thrown when the given reference is already registered as a new reference or already existed. """ class ReadOnlyProxyException(Exception): """ Exception raised when the proxy is for read only. """ class IntegrityConstraintError(RuntimeError): """ Runtime Error raised when the given value violates a integrity constraint. """ class NonRefreshableEntity(Exception): """ Exception thrown when the UOW attempts to refresh a non-refreshable entity """ class EntityAlreadyRecognized(Warning): """ Warning raised when the entity with either a designated ID or a designated session is provided to Repository.post """ class EntityNotRecognized(Warning): """ Warning raised when the entity without either a designated ID or a designated session is provided to Repository.put or Repository.delete """PKX MHql#l#passerine/db/expression.py# -*- coding: utf-8 -*- import json import re class InvalidExpressionError(Exception): """ Generic Invalid Expression Error """ class DataObject(object): @property def in_json(self): raise RuntimeError('Unknown data object') def __str__(self): return str(self.in_json) def __repr__(self): return '{}({})'.format(self.__class__.__name__, self.__str__()) class ExpressionOperand(object): OP_EQ = '=' OP_NE = '!=' OP_GE = '>=' OP_GT = '>' OP_LE = '<=' OP_LT = '<' OP_IN = 'in' OP_NOT_IN = 'not in' OP_SQL_LIKE = 'like' OP_REGEXP_LIKE = 'rlike' OP_INDEX_SEARCH = 'indexed with' class ExpressionType(object): IS_PARAMETER = 'param' IS_PROPERTY_PATH = 'path' IS_DATA = 'data' class Expression(DataObject): """ Query Expression :param passerine.db.expression.ExpressionPart left: the left part :param passerine.db.expression.ExpressionPart right: the right part :param str operand: the generic operand """ def __init__(self, left, operand, right): self.left = left self.operand = operand self.right = right @property def in_json(self): return { 'left': self.left, 'right': self.right, 'operand': self.operand } def __str__(self): return '{left} {operand} {right}'.format( left = self.left.original, right = self.right.original, operand = self.operand ) class ExpressionPart(DataObject): """ Query Expression :param str original: the original query :param str kind: the type of the part :param value: the parameter value only for a data part :param str alias: the entity alias for a property part or the name of the parameter of a parameter part """ def __init__(self, original, kind, value, alias): self.original = original self.kind = kind self.value = value self.alias = alias @property def in_json(self): return { 'original': self.original, 'kind': self.kind, 'value': self.value, 'alias': self.alias } class ExpressionSet(DataObject): """ Representation of Analyzed Expression """ def __init__(self, expressions): self.properties = {} self.parameters = [] self.expressions = expressions @property def in_json(self): return { 'properties': self.properties, 'parameters': self.parameters, 'expressions': self.expressions } class Criteria(object): """ Expression Criteria Support operands: =, <=, <, >, >=, in, like (SQL-like string pattern), rlike (Regular-expression pattern), indexed with (only for Riak) """ def __init__(self): self._is_updated = False self._sub_expressions = [] self._analyzed_map = None self._re_parameter = re.compile('^:[a-zA-Z0-9_]+$') self._re_root_path = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$') self._re_property_path = re.compile('^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$') self._re_statement = re.compile( '^\s*(?P.+)\s+(?P{eq}|{ne}|{ge}|{gt}|{le}|{lt}|{xin}|{xnin}|{like}|{rlike}|{indexed})\s+(?P.+)\s*$'.format( eq = ExpressionOperand.OP_EQ, ne = ExpressionOperand.OP_NE, ge = ExpressionOperand.OP_GE, gt = ExpressionOperand.OP_GT, le = ExpressionOperand.OP_LE, lt = ExpressionOperand.OP_LT, xin = ExpressionOperand.OP_IN, xnin = ExpressionOperand.OP_NOT_IN, like = ExpressionOperand.OP_SQL_LIKE, rlike = ExpressionOperand.OP_REGEXP_LIKE, indexed = ExpressionOperand.OP_INDEX_SEARCH ), re.IGNORECASE ) self._re_property_path_delimiter = re.compile('\.') def get_analyzed_version(self): if self._is_updated: return self._analyzed_map analyzed_expression = ExpressionSet(self._sub_expressions) # Scan for all property paths and parameters. for se in self._sub_expressions: self._scan_for_property_paths_and_parameters(analyzed_expression, se.left) self._scan_for_property_paths_and_parameters(analyzed_expression, se.right) if not analyzed_expression.properties: raise InvalidExpressionError('There must be at least one property path. It is prone to query injection.') self._analyzed_map = analyzed_expression return self._analyzed_map def _scan_for_property_paths_and_parameters(self, analyzed_expression, sub_expression_part): """ Search for all property paths and parameters. """ # Search for all referred property paths. if sub_expression_part.kind == ExpressionType.IS_PROPERTY_PATH: property_path = sub_expression_part.original analyzed_expression.properties[property_path] = None # Search for all referred property paths. if sub_expression_part.kind == ExpressionType.IS_PARAMETER: parameter_name = sub_expression_part.original[1:] analyzed_expression.parameters.append(parameter_name) def expect(self, statement): self._is_updated = False expr = self._compile(statement) self._sub_expressions.append(expr) @property def _fixed_syntax_operands(self): return ('in', 'like', 'rlike', 'indexed with') def _compile(self, statement): fixed_syntax_operands = self._fixed_syntax_operands expr = self._parse(statement) try: expr.left = self._parse_side(expr.left) except InvalidExpressionError as exception: raise InvalidExpressionError('The left side of the expression cannot be parsed.') try: expr.right = self._parse_side(expr.right) except InvalidExpressionError as exception: raise InvalidExpressionError('The left side of the expression cannot be parsed.') # Validate the syntax on the fixed syntaxes. if expr.operand in fixed_syntax_operands: if expr.left.kind != ExpressionType.IS_PROPERTY_PATH: raise InvalidExpressionError('The property path must be on the left of the operand.') if expr.right.kind == ExpressionType.IS_PROPERTY_PATH: raise InvalidExpressionError('The property path cannot be on the right of the operand.') # If the left side refers to the root path but not for the index search, # the method will raise the invalid expression error. if expr.left.kind == ExpressionType.IS_PROPERTY_PATH \ and '.' not in expr.left.original \ and expr.operand != ExpressionOperand.OP_INDEX_SEARCH: raise InvalidExpressionError('The property path to the root entity can only be used by index search.') # The property path must be in the expression. if expr.left.kind != ExpressionType.IS_PROPERTY_PATH and expr.right.kind != ExpressionType.IS_PROPERTY_PATH: raise InvalidExpressionError('The property path must be in the expression.') return expr def _parse_side(self, sub_statement): kind = ExpressionType.IS_DATA if self._re_parameter.match(sub_statement): kind = ExpressionType.IS_PARAMETER elif self._re_property_path.match(sub_statement) or self._re_root_path.match(sub_statement): kind = ExpressionType.IS_PROPERTY_PATH if kind != ExpressionType.IS_DATA: alias = self._re_property_path_delimiter.split(sub_statement)[0]\ if self._re_property_path_delimiter.search(sub_statement) \ else sub_statement[1:] return self._create_expression_part({ 'original': sub_statement, 'kind': kind, 'value': None, 'alias': alias }) decoded_data = None try: decoded_data = json.loads(sub_statement) except ValueError as exception: raise InvalidExpressionError('Unable to decode the data.') return self._create_expression_part({ 'original': sub_statement, 'kind': kind, 'value': decoded_data, 'alias': None }) def _create_expression_part(self, parameters): return ExpressionPart(**parameters) def _parse(self, statement): matches = self._re_statement.match(statement) if not matches: raise InvalidExpressionError('Incomplete statement: {}'.format(statement)) raw_expr = matches.groupdict() expression = Expression(**raw_expr) return expressionPKX MHWpasserine/db/entity.py""" :Author: Juti Noppornpitak """ import inspect from imagination.decorator.validator import restrict_type from passerine.db.common import PseudoObjectId from passerine.db.exception import LockedIdException from passerine.db.metadata.helper import EntityMetadataHelper def get_collection_name(cls): raise RuntimeError('obsolete') return cls.__collection_name__ def get_relational_map(cls): raise RuntimeError('obsolete') return cls.__relational_map__ def entity(*args, **kwargs): """ Entity decorator :param collection_name: the name of the collection :type collection_name: str :return: the decorated object :rtype: object """ # Get the first parameter. first_param = args[0] if args else None # If the first parameter is really a reference to a class, then instantiate # the singleton instance. if args and inspect.isclass(first_param) and isinstance(first_param, type): class_reference = first_param return prepare_entity_class(class_reference) # Otherwise, use the closure to handle the parameter. def decorator(class_reference): return prepare_entity_class(class_reference, *args, **kwargs) return decorator def prepare_entity_class(cls, collection_name=None, indexes=[]): """ Create a entity class :param cls: the document class :type cls: object :param collection_name: the name of the corresponding collection where the default is the lowercase version of the name of the given class (cls) :type collection_name: str The object decorated with this decorator will be automatically provided with a few additional attributes. =================== ======== =================== ==== ================================= Attribute Access Description Read Write =================== ======== =================== ==== ================================= id Instance Document Identifier Yes Yes, ONLY ``id`` is undefined. __t3_orm_meta__ Static Tori 3's Metadata Yes ONLY the property of the metadata __session__ Instance DB Session Yes Yes, but NOT recommended. =================== ======== =================== ==== ================================= The following attributes might stay around but are deprecated as soon as the stable Tori 3.0 is released. =================== ======== =================== ==== ================================= Attribute Access Description Read Write =================== ======== =================== ==== ================================= __collection_name__ Static Collection Name Yes Yes, but NOT recommended. __relational_map__ Static Relational Map Yes Yes, but NOT recommended. __indexes__ Static Indexing List Yes Yes, but NOT recommended. =================== ======== =================== ==== ================================= ``__session__`` is used to resolve the managing rights in case of using multiple sessions simutaneously. For example, .. code-block:: python @entity class Note(object): def __init__(self, content, title=''): self.content = content self.title = title where the collection name is automatically defined as "note". .. versionchanged:: 3.0 The way Tori stores metadata objects in ``__collection_name__``, ``__relational_map__`` and ``__indexes__`` are now ignored by the ORM in favour of ``__t3_orm_meta__`` which is an entity metadata object. This change is made to allow easier future development. .. tip:: You can define it as "notes" by replacing ``@entity`` with ``@entity('notes')``. """ if not cls: raise ValueError('Expecting a valid type') def get_id(self): return self.__dict__['_id'] if '_id' in self.__dict__ else None def set_id(self, id): """ Define the document ID if the original ID is not defined. :param id: the ID of the document. """ if '_id' in self.__dict__ and self.__dict__['_id']\ and not isinstance(self.__dict__['_id'], PseudoObjectId): raise LockedIdException('The ID is already assigned and cannot be changed.') self._id = id cls.__session__ = None EntityMetadataHelper.imprint( cls, collection_name or cls.__name__.lower(), indexes ) cls.id = property(get_id, set_id) return cls class Entity(object): """ Dynamic-attribute Basic Entity :param attributes: key-value dictionary :type attributes: dict Here is an example on how to use this class. .. code-block:: python @entity class Note(Entity): pass """ def __init__(self, **attributes): for name in attributes: self.__setattr__(name, attributes[name]) class Index(object): """ Index :param field_map: the map of field to index type :type field_map: dict :param unique: the unique flag :type unique: bool Unless a field is not in the map of fixed orders, the index will instruct the repository to ensure all combinations of indexes are defined whenever is necessary. """ def __init__(self, field_map, unique=False): self._field_map = field_map self._unique = unique @property def field_map(self): return self._field_map @property def unique(self): return self._unique def to_list(self): index_map = [] for field in self._field_map: index_map.append((field, self._field_map[field])) return index_map class BasicAssociation(object): """ Basic Association :param origin: The origin of the association :type origin: object :param destination: The destination (endpoint) of the association :type destination: object .. note:: This class is used automatically by the association mapper. """ def __init__(self, origin, destination): self.origin = origin self.destination = destinationPKX MHCgccpasserine/db/uow.py# -*- coding: utf-8 -*- from time import time from threading import Lock as ThreadLock from passerine.graph import DependencyNode as BaseDependencyNode, DependencyManager from passerine.db.common import Serializer, PseudoObjectId, ProxyObject from passerine.db.entity import BasicAssociation from passerine.db.exception import UOWRepeatedRegistrationError, UOWUpdateError, UOWUnknownRecordError, IntegrityConstraintError from passerine.db.mapper import CascadingType from passerine.db.metadata.helper import EntityMetadataHelper class Record(object): serializer = Serializer(0) STATUS_CLEAN = 1 STATUS_DELETED = 2 STATUS_DIRTY = 3 STATUS_NEW = 4 STATUS_IGNORED = 5 STATUS_LABEL_MAP = { 1: 'clean', 2: 'deleted', 3: 'dirty', 4: 'new', 5: 'ignored' } def __init__(self, entity, status): self.entity = entity self.status = status self.updated = time() self.original_data_set = Record.serializer.encode(self.entity) self.original_extra_association = Record.serializer.extra_associations(self.entity) def mark_as(self, status): self.status = status self.updated = time() def update(self): self.original_data_set = Record.serializer.encode(self.entity) self.original_extra_association = Record.serializer.extra_associations(self.entity) self.mark_as(Record.STATUS_CLEAN) class DependencyNode(BaseDependencyNode): """ Dependency Node This is designed to be bi-directional to maximize flexibility on traversing the graph. """ def __init__(self, record): super(DependencyNode, self).__init__() self.record = record @property def object_id(self): return self.record.entity.id @property def status(self): return self.record.status def _disavow_connection(self, node): return node.status == Record.STATUS_DELETED def __eq__(self, other): return self.record.entity.__class__ == other.record.entity.__class__ and self.object_id == other.object_id def __ne__(self, other): return self.record.entity.__class__ != other.record.entity.__class__ or self.object_id != other.object_id def __hash__(self): return self.created_at class UnitOfWork(object): """ Unit of Work This Unit of Work (UOW) is designed specifically for non-relational databases. .. note:: It is the design decision to make sub-commit methods available so that when it is used with Imagination Framework, the other Imagination entity may intercept before or after actually committing data. In the other word, Imagination Framework acts as an event controller for any actions (public methods) of this class. """ serializer = Serializer(0) def __init__(self, entity_manager): # given property self._em = entity_manager # caching properties self._record_map = {} # Object Hash => Record self._object_id_map = {} # str(ObjectID) => Object Hash self._dependency_map = None # Locks self._blocker_activated = False self._blocking_lock = ThreadLock() self._operational_lock = ThreadLock() def _freeze(self): if not self._blocker_activated: return self._operational_lock.acquire() def _unfreeze(self): if not self._blocker_activated: return self._operational_lock.release() def refresh(self, entity): """ Refresh the entity .. note:: This method :param entity: the target entity :type entity: object """ self._freeze() record = self.retrieve_record(entity) if record.status == Record.STATUS_DELETED: return # Ignore the entity marked as deleted. elif record.status not in [Record.STATUS_CLEAN, Record.STATUS_DIRTY]: raise NonRefreshableEntity('The current record is not refreshable.') collection = self._em.collection(entity.__class__) updated_data_set = collection.driver.find_one(collection.name, {'_id': entity.id}) # Reset the attributes. for attribute_name in updated_data_set: entity.__setattr__(attribute_name, updated_data_set[attribute_name]) # Remove the non-existed attributes. for attribute_name in record.original_data_set: if attribute_name in updated_data_set: continue entity.__delattr__(attribute_name) # Update the original data set and reset the status if necessary. record.original_data_set = Record.serializer.encode(entity) record.extra_association = Record.serializer.extra_associations(entity) if record.status == Record.STATUS_DIRTY: record.mark_as(Record.STATUS_CLEAN) # Remap any one-to-many or many-to-many relationships. self._em.apply_relational_map(entity) self._cascade_operation(entity, CascadingType.REFRESH) self._unfreeze() def register_new(self, entity): """ Register a new entity :param entity: the entity to register :type entity: object """ self._freeze() self._register_new(entity) self._unfreeze() def _register_new(self, entity): """ Register a entity as new (protected) .. warning:: This method bypasses the thread lock imposed in the public method. It is for internal use only. :param entity: the target entity :type entity: object """ uid = self._retrieve_entity_guid(entity) if self.has_record(entity): raise UOWRepeatedRegistrationError('Could not mark the entity as new.') if not entity.id: entity.id = self._generate_pseudo_object_id() self._record_map[uid] = Record(entity, Record.STATUS_NEW) # Map the pseudo object ID to the entity. self._object_id_map[self._convert_object_id_to_str(entity.id, entity)] = uid self._cascade_operation(entity, CascadingType.PERSIST) def register_dirty(self, entity): """ Register the entity with the dirty bit :param entity: the entity to register :type entity: object """ self._freeze() record = self.retrieve_record(entity) if record.status == Record.STATUS_NEW: try: return self.register_new(entity) except UOWRepeatedRegistrationError as exception: pass elif record.status in [Record.STATUS_CLEAN, Record.STATUS_DELETED]: record.mark_as(Record.STATUS_DIRTY) self._cascade_operation(entity, CascadingType.PERSIST) self._unfreeze() def register_clean(self, entity): """ Register the entity with the clean bit :param entity: the entity to register :type entity: object """ uid = self._retrieve_entity_guid(entity) if uid in self._record_map: raise UOWRepeatedRegistrationError('Could not mark the entity as clean') self._record_map[uid] = Record(entity, Record.STATUS_CLEAN) # Map the real object ID to the entity self._object_id_map[self._convert_object_id_to_str(entity.id, entity)] = uid def register_deleted(self, entity): """ Register the entity with the removal bit :param entity: the entity to register :type entity: object """ self._freeze() self._register_deleted(entity) self._unfreeze() def _register_deleted(self, entity): """ Register a entity as deleted (no lock) .. warning:: This method bypasses the thread lock imposed in the public method. It is for internal use only. :param entity: the target entity :type entity: object """ record = self.retrieve_record(entity) if record.status == Record.STATUS_NEW or isinstance(entity.id, PseudoObjectId): record.mark_as(Record.STATUS_IGNORED) else: record.mark_as(Record.STATUS_DELETED) self._cascade_operation(entity, CascadingType.DELETE) def _cascade_operation(self, reference, cascading_type): entity = reference if isinstance(reference, ProxyObject): entity = reference._actual if not EntityMetadataHelper.hasMetadata(entity): return entity_meta = EntityMetadataHelper.extract(entity) relational_map = entity_meta.relational_map for property_name in relational_map: guide = relational_map[property_name] if guide.inverted_by: continue actual_data = entity.__getattribute__(property_name) reference = self.hydrate_entity(actual_data) if not guide.cascading_options\ or cascading_type not in guide.cascading_options\ or not reference: continue if isinstance(reference, list): for sub_reference in actual_data: self._forward_operation( self.hydrate_entity(sub_reference), cascading_type, guide.target_class ) continue self._forward_operation( reference, cascading_type, guide.target_class ) def _forward_operation(self, reference, cascading_type, expected_class): if cascading_type == CascadingType.PERSIST: if type(reference) is not expected_class: reference_type = type(reference) raise IntegrityConstraintError( 'Expected an instance of class {} ({}) but received one of {} ({})'.format( expected_class.__name__, expected_class.__module__, reference_type.__name__, reference_type.__module__ ) ) if self.is_new(reference): try: self.register_new(reference) except UOWRepeatedRegistrationError as exception: pass else: self.register_dirty(reference) elif cascading_type == CascadingType.DELETE: self.register_deleted(reference) elif cascading_type == CascadingType.REFRESH: self.refresh(reference) def is_new(self, reference): return not reference.id or isinstance(reference.id, PseudoObjectId) def hydrate_entity(self, reference): return reference._actual if isinstance(reference, ProxyObject) else reference def commit(self): self._blocking_lock.acquire() self._blocker_activated = True self._freeze() # Make changes on the normal entities. self._commit_changes() # Then, make changes on external associations. self._add_or_remove_associations() self._commit_changes(BasicAssociation) # Synchronize all records self._synchronize_records() self._unfreeze() self._blocker_activated = False self._blocking_lock.release() def _commit_changes(self, expected_class=None): # Load the sub graph of supervised collections. for c in self._em.repositories(): if not c.has_cascading(): continue c.find(force_loading=True) commit_order = self._compute_order() # Commit changes to nodes. for commit_node in commit_order: uid = self._retrieve_entity_guid_by_id(commit_node.object_id, commit_node.record.entity.__class__) record = self._record_map[uid] if expected_class and not isinstance(record.entity, expected_class): continue collection = self._em.collection(record.entity.__class__) change_set = self._compute_change_set(record) if record.status == Record.STATUS_NEW: self._synchronize_new( collection, record.entity, change_set ) elif record.status == Record.STATUS_DIRTY and change_set: self._synchronize_update( collection, record.entity.id, record.original_data_set, change_set ) elif record.status == Record.STATUS_DIRTY and not change_set: record.mark_as(Record.STATUS_CLEAN) elif record.status == Record.STATUS_DELETED and commit_node.score == 0: self._synchronize_delete(collection, record.entity.id) elif record.status == Record.STATUS_DELETED and commit_node.score > 0: record.mark_as(Record.STATUS_CLEAN) def _synchronize_new(self, repository, entity, change_set): """ Synchronize the new / unsupervised data :param repository: the target repository :param entity: the entity :param change_set: the change_set representing the entity """ pseudo_key = self._convert_object_id_to_str(entity.id, entity) object_id = repository.driver.insert(repository.name, change_set) entity.id = object_id # update the entity ID actual_key = self._convert_object_id_to_str(object_id, entity) self._object_id_map[actual_key] = self._object_id_map[pseudo_key] def _synchronize_update(self, repository, object_id, old_data_set, new_data_set): """ Synchronize the updated data :param repository: the target repository :param object_id: the object ID :param old_data_set: the original data (for event interception) :param new_data_set: the updated data """ repository.driver.update( repository.name, {'_id': object_id}, new_data_set ) def _synchronize_delete(self, repository, object_id): """ Synchronize the deleted data :param repository: the target repository :param object_id: the object ID """ repository.driver.remove(repository.name, {'_id': object_id}) def _synchronize_records(self): writing_statuses = [Record.STATUS_NEW, Record.STATUS_DIRTY] removed_statuses = [Record.STATUS_DELETED, Record.STATUS_IGNORED] uid_list = list(self._record_map.keys()) for uid in uid_list: record = self._record_map[uid] if record.status in removed_statuses: del self._record_map[uid] elif record.status in writing_statuses: record.update() def retrieve_record(self, entity): uid = self._retrieve_entity_guid(self._em._force_load(entity)) if uid not in self._record_map: raise UOWUnknownRecordError('Unable to retrieve the record for this entity.') return self._record_map[uid] def delete_record(self, entity): uid = self._retrieve_entity_guid(entity) if uid not in self._record_map: raise UOWUnknownRecordError('Unable to retrieve the record for this entity.') del self._record_map[uid] def has_record(self, entity): return self._retrieve_entity_guid(entity) in self._record_map def find_recorded_entity(self, object_id, cls): object_key = self._convert_object_id_to_str(object_id, cls=cls) if object_key in self._object_id_map: try: return self._record_map[self._object_id_map[object_key]] except KeyError as exception: # This exception is raised possibly due to that the record is deleted. del self._object_id_map[object_key] return None def _compute_order(self): self._construct_dependency_graph() # After constructing the dependency graph (as a supposedly directed acyclic # graph), do the topological sorting from the dependency graph. return DependencyManager.get_order(self._dependency_map) def _compute_change_set(self, record): current_set = Record.serializer.encode(record.entity) if record.status == Record.STATUS_NEW: return current_set elif record.status == Record.STATUS_DELETED: return record.entity.id original_set = dict(record.original_data_set) change_set = { '$set': {}, '$unset': {} } original_property_set = set(original_set.keys()) current_property_set = set(current_set.keys()) expected_property_list = original_property_set.intersection(current_property_set) expected_property_list = expected_property_list.union(current_property_set.difference(original_property_set)) unexpected_property_list = original_property_set.difference(current_property_set) # Add or update properties for name in expected_property_list: if name in original_set and original_set[name] == current_set[name]: continue change_set['$set'][name] = current_set[name] # Remove unwanted properties for name in unexpected_property_list: change_set['$unset'][name] = 1 directive_list = list(change_set.keys()) # Clean up the change set for directive in directive_list: if change_set[directive]: continue del change_set[directive] return change_set def _compute_connection_changes(self, record): """ Compute changes in external associations originated from the entity of the current record This method is designed specifically to deal with many-to-many association by adding or removing associative entities which their origin is from the entity from the current record. :param record: the UOW record :type record: passerine.db.uow.Record """ current = Record.serializer.extra_associations(record.entity) original = dict(record.original_extra_association) change_set = {} original_property_set = set(original.keys()) current_property_set = set(current.keys()) expected_property_list = original_property_set.intersection(current_property_set) expected_property_list = expected_property_list.union(current_property_set.difference(original_property_set)) unexpected_property_list = expected_property_list if record.status == Record.STATUS_DELETED else original_property_set.difference(current_property_set) # Find new associations for name in expected_property_list: current_set = set(current[name]) original_set = set(original[name]) diff_additions = current_set diff_deletions = [] if record.status != Record.STATUS_NEW: diff_additions = current_set.difference(original_set) diff_deletions = original_set.difference(current_set) change_set[name] = { 'action': 'update', 'new': diff_additions, 'deleted': diff_deletions } # Find new associations for name in unexpected_property_list: change_set[name] = { 'action': 'purge' } return change_set def _load_extra_associations(self, record, change_set): origin_id = record.entity.id relational_map = EntityMetadataHelper.extract(record.entity).relational_map for property_name in relational_map: if property_name not in change_set: continue property_change_set = change_set[property_name] guide = relational_map[property_name] repository = self._em.collection(guide.association_class.cls) if property_change_set['action'] == 'update': for unlinked_destination_id in property_change_set['deleted']: association = repository.filter_one({'origin': origin_id, 'destination': unlinked_destination_id}) if not association: continue self._register_deleted(association) for new_destination_id in property_change_set['new']: association = repository.new(origin=origin_id, destination=new_destination_id) self._register_new(association) return elif property_change_set['action'] == 'purge': for association in repository.filter({'origin': origin_id}): self._register_deleted(association) return raise RuntimeError('Unknown changes on external associations for {}'.format(origin_id)) def _add_or_remove_associations(self): # Find out if UOW needs to deal with extra records (associative collection). uid_list = list(self._record_map.keys()) for uid in uid_list: record = self._record_map[uid] if record.status == Record.STATUS_CLEAN: continue change_set = self._compute_connection_changes(record) if not change_set: continue self._load_extra_associations(record, change_set) def _retrieve_entity_guid(self, entity): return self._retrieve_entity_guid_by_id(entity.id, entity.__class__)\ if isinstance(entity, ProxyObject)\ else hash(entity) def _retrieve_entity_guid_by_id(self, id, cls): return self._object_id_map[self._convert_object_id_to_str(id, cls=cls)] def _generate_pseudo_object_id(self): return PseudoObjectId() def _convert_object_id_to_str(self, object_id, entity=None, cls=None): class_hash = 'generic' if not cls and entity: cls = entity.__class__ if cls: metadata = EntityMetadataHelper.extract(cls) class_hash = metadata.collection_name object_key = '{}/{}'.format(class_hash, str(object_id)) return object_key def _construct_dependency_graph(self): self._dependency_map = {} for uid in self._record_map: record = self._record_map[uid] object_id = self._convert_object_id_to_str(record.entity.id, record.entity) current_set = Record.serializer.encode(record.entity) extra_association = Record.serializer.extra_associations(record.entity) # Register the current entity into the dependency map if it's never # been registered or eventually has no dependencies. if object_id not in self._dependency_map: self._dependency_map[object_id] = DependencyNode(record) relational_map = EntityMetadataHelper.extract(record.entity).relational_map if not relational_map: continue # Go through the relational map to establish relationship between dependency nodes. for property_name in relational_map: guide = relational_map[property_name] # Ignore a property from reverse mapping. if guide.inverted_by: continue # ``data`` can be either an object ID or list. data = current_set[property_name] if not data: # Ignore anything evaluated as False. continue elif not isinstance(data, list): other_uid = self._retrieve_entity_guid_by_id(data, guide.target_class) other_record = self._record_map[other_uid] self._register_dependency(record, other_record) continue for dependency_object_id in data: other_uid = self._retrieve_entity_guid_by_id(dependency_object_id, guide.target_class) other_record = self._record_map[other_uid] self._register_dependency(record, other_record) return self._dependency_map def _retrieve_dependency_order(self, node, priority_order): if node.walked: return node.walked = True initial_order = list(node.adjacent_nodes) for adjacent_node in initial_order: self._retrieve_dependency_order(adjacent_node, priority_order) if node not in priority_order: priority_order.append(node) def _register_dependency(self, a, b): key_a = self._convert_object_id_to_str(a.entity.id, a.entity) key_b = self._convert_object_id_to_str(b.entity.id, b.entity) if key_a not in self._dependency_map: self._dependency_map[key_a] = DependencyNode(a) if key_b not in self._dependency_map: self._dependency_map[key_b] = DependencyNode(b) self._dependency_map[key_a].connect(self._dependency_map[key_b]) PKX MH:++passerine/db/common.py# -*- coding: utf-8 -*- """ :Author: Juti Noppornpitak :Stability: Stable """ from bson import ObjectId from passerine.data.serializer import ArraySerializer from passerine.db.exception import ReadOnlyProxyException from passerine.db.metadata.helper import EntityMetadataHelper class Serializer(ArraySerializer): """ Object Serializer for Entity """ def extra_associations(self, data, stack_depth=0): if not isinstance(data, object): raise TypeError('The provided data must be an object') returnee = {} relational_map = EntityMetadataHelper.extract(data).relational_map if self._is_entity(data) else {} extra_associations = {} for name in dir(data): # Skip all protected/private/reserved properties. if self._is_preserved_property(name): continue guide = self._retrieve_guide(relational_map, name) # Skip all properties without an associative guide or with reverse mapping or without pseudo association class. if not guide or guide.inverted_by or not guide.association_class: continue property_reference = data.__getattribute__(name) # Skip all callable properties and non-list properties if callable(property_reference) or not isinstance(property_reference, list): continue # With a valid association class, this property has the many-to-many relationship with the other entity. extra_associations[name] = [] for destination in property_reference: extra_associations[name].append(destination.id) return extra_associations def encode(self, data, stack_depth=0, convert_object_id_to_str=False): """ Encode data into dictionary and list. :param data: the data to encode :param stack_depth: traversal depth limit :param convert_object_id_to_str: flag to convert object ID into string """ if not isinstance(data, object): raise TypeError('The provided data must be an object') returnee = {} relational_map = EntityMetadataHelper.extract(data).relational_map if self._is_entity(data) else {} for name in dir(data): # Skip all protected/private/reserved properties. if self._is_preserved_property(name): continue guide = self._retrieve_guide(relational_map, name) # Skip all pseudo properties used for reverse mapping. if guide and guide.inverted_by: continue property_reference = data.__getattribute__(name) is_list = isinstance(property_reference, list) value = None # Skip all callable properties if callable(property_reference): continue # For one-to-many relationship, this property relies on the built-in list type. if is_list: value = [] for item in property_reference: value.append(self._process_value(data, item, stack_depth, convert_object_id_to_str)) else: value = self._process_value(data, property_reference, stack_depth, convert_object_id_to_str) returnee[name] = value # If this is not a pseudo object ID, add the reserved key '_id' with the property 'id' . if data.id and not isinstance(data.id, PseudoObjectId): returnee['_id'] = self._process_value(data, data, stack_depth, convert_object_id_to_str) return returnee def _retrieve_guide(self, relational_map, name): return relational_map[name] if name in relational_map else None def _is_preserved_property(self, name): return name[0] == '_' or name == 'id' def _is_entity(self, data): return EntityMetadataHelper.hasMetadata(data) def _process_value(self, data, value, stack_depth, convert_object_id_to_str): is_proxy = isinstance(value, ProxyObject) is_document = isinstance(data, object) and self._is_entity(data) processed_data = value if value and not self._is_primitive_type(value): if self._max_depth and stack_depth >= self._max_depth: processed_data = value.encode('utf-8', 'replace') if self._is_string(value) else value elif is_proxy or is_document: processed_data = value.id if isinstance(processed_data, ObjectId) and convert_object_id_to_str: processed_data = str(processed_data) else: processed_data = self.encode(value, stack_depth + 1) elif isinstance(value, ObjectId) and convert_object_id_to_str: processed_data = str(value) return processed_data def default_primitive_types(self): return super(Serializer, self).default_primitive_types() + [PseudoObjectId, ObjectId] class PseudoObjectId(ObjectId): """ Pseudo Object ID This class extends from :class:`bson.objectid.ObjectId`. This is used to differentiate stored entities and new entities. """ def __str__(self): return 'P-{}'.format(super(PseudoObjectId, self).__str__()) def __repr__(self): return "PseudoObjectId('%s')" % (str(self),) class ProxyObject(object): """ Proxy Collection This class is designed to only load the entity whenever the data access is required. :param session: the managed session :type session: passerine.db.session.Session :param cls: the class to map the data :type cls: type :param object_id: the object ID :param read_only: the read-only flag :type read_only: bool :param cascading_options: the cascading options :type cascading_options: list or tuple :param is_reverse_proxy: the reverse proxy flag :type is_reverse_proxy: bool """ def __init__(self, session, cls, object_id, read_only, cascading_options, is_reverse_proxy): if isinstance(cls, ProxyObject) or not object_id: raise RuntimeError('Cannot initiate a proxy') self.__dict__['_class'] = cls self.__dict__['_collection'] = session.collection(cls) self.__dict__['_object_id'] = object_id self.__dict__['_object'] = None self.__dict__['_read_only'] = read_only self.__dict__['_cascading_options'] = cascading_options self.__dict__['_is_reverse_proxy'] = is_reverse_proxy def __get_object(self): if not self.__dict__['_object_id']: raise RuntimeError('Cannot load the proxy') if not self.__dict__['_object']: entity = self._collection.get(self.__dict__['_object_id']) if entity: self.__dict__['_object'] = entity return self.__dict__['_object'] def __getattr__(self, item): if item == '_actual': return self.__get_object() #elif item == 'id': # return self.__dict__['_object_id'] elif item[0] == '_': return self.__dict__[item] elif not self.__dict__['_object_id'] or not self.__get_object(): return None return self.__get_object().__getattribute__(item) def __setattr__(self, key, value): if self._read_only: raise ReadOnlyProxyException('The proxy is read only.') self.__get_object().__setattr__(key, value) class ProxyCollection(list): """ Proxy Collection This collection is extended from the built-in class :class:`list`, designed to only load the associated data whenever is required. :param session: the managed session :type session: passerine.db.session.Session :param origin: the origin of the association :type origin: object :param guide: the relational guide :type guide: passerine.db.mapper.RelatingGuide .. note:: To replace with criteria and driver """ def __init__(self, session, origin, guide): self._session = session self._origin = origin self._guide = guide self._loaded = False def reload(self): """ Reload the data list .. warning:: This method is **not recommended** to be called directly. Use :meth:`passerine.db.session.Session.refresh` on the owned object instead. """ while len(self): self.pop(0) self._loaded = False self._prepare_list() def _prepare_list(self): if self._loaded: return self._loaded = True association_class = self._guide.association_class.cls collection = self._session.collection(association_class) if self._guide.inverted_by: criteria = collection.new_criteria() criteria.expect('e.destination = :destination') criteria.define('destination', self._origin.id) mapping_list = collection.find(criteria) self.extend([ ProxyFactory.make(self._session, association.origin, self._guide) for association in mapping_list ]) return criteria = {'origin': self._origin.id} mapping_list = collection.filter(criteria) self.extend([ ProxyFactory.make(self._session, association.destination, self._guide) for association in mapping_list ]) def __iter__(self): self._prepare_list() return super(ProxyCollection, self).__iter__() def __len__(self): self._prepare_list() return super(ProxyCollection, self).__len__() def __contains__(self, item): self._prepare_list() return super(ProxyCollection, self).__contains__(item) def __delitem__(self, key): self._prepare_list() return super(ProxyCollection, self).__delitem__(key) def __getitem__(self, item): self._prepare_list() return super(ProxyCollection, self).__getitem__(item) def __getslice__(self, i, j): self._prepare_list() return super(ProxyCollection, self).__getslice__(i, j) def __setitem__(self, key, value): self._prepare_list() super(ProxyCollection, self).__setitem__(key, value) class ProxyFactory(object): """ Proxy Factory This factory is to create a proxy object. :param session: the managed session :type session: passerine.db.session.Session :param id: the object ID :param mapping_guide: the relational guide :type mapping_guide: passerine.db.mapper.RelatingGuide """ @staticmethod def make(session, id, mapping_guide): is_reverse_proxy = mapping_guide.inverted_by != None return ProxyObject( session, mapping_guide.target_class, id, mapping_guide.read_only or is_reverse_proxy, mapping_guide.cascading_options, is_reverse_proxy )PKX MHGpasserine/db/wrapper.py""" Wrappers for SQLAlchemy ####################### :Author: Juti Noppornpitak """ from os import getpid from threading import current_thread from sqlalchemy import create_engine from sqlalchemy.exc import OperationalError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from passerine.common import get_logger Entity = declarative_base() class Repository(object): """Relational Database Service This is compatible with SQLAlchemy 0.7+. This service provides basic functionality to interact a relational database. It also heavily uses lazy loading for the sake of faster startup and resource efficiency. :param url: a URL to the database, possibly including location, credential and name. The default value is ``sqlite:///:memory:``. :type url: str :param echo: the flag to echo database operation. The default value is ``False`` :type echo: bool """ def __init__(self, url='sqlite:///:memory:', echo=False): self._logger = get_logger('%s.%s' % (__name__, self.__class__.__name__)) self._echo = echo self._url = url self._engine = None self._session_maker = None self._sessions = {} self._batch_reflected = False self._reflected_entities = [] @property def session_key(self): return '%s.%s' % (getpid(), current_thread().ident) @property def engine(self): """ Get the SQLAlchemy engine. :rtype: sqlalchemy.engine.base.Engine .. note:: With the service, it is not recommended to directly use this method. """ if not self._engine: self._engine = create_engine(self._url, echo=self._echo) return self._engine @property def url(self): """ Get the connecting URL. :return: the URL :rtype: string|unicode """ return self._url @url.setter def url(self, new_url): """ Set the connecting URL. :param new_url: a new URL for the database connection. """ self._url = new_url self._engine = None def reflect(self, entity_type=None): """ Create the table if necessary. :rtype: True if the reflection is made. """ if not self._batch_reflected and not entity_type: self._batch_reflected = True Entity.metadata.create_all(self.engine) return True if self.reflected(entity_type): return False self._reflected_entities.append(entity_type.__name__) try: entity_type.__table__.create(self.engine) except OperationalError: return False return True def reflected(self, entity_type=None): return entity_type.__name__ in self._reflected_entities \ if entity_type \ else self._batch_reflected @property def session(self): """ Get a SQLAlchemy session for the current connection. :rtype: sqlalchemy.orm.session.Session :return: a session for the connection. """ engine = self.engine if not self._session_maker: self._session_maker = sessionmaker(bind=engine) session_key = self.session_key if session_key not in self._sessions: self._logger.info('Create new DB session: %s' % session_key) self._sessions[session_key] = self._session_maker() return self._sessions[session_key] def query(self, *entity_types): """ Retrieve the query object for the given type of entities. :param entity_type: the type of entity :type entity_type: list :rtype: sqlalchemy.orm.query.Query :return: The query object for the given type of entities. """ for entity_type in entity_types: if entity_type.__name__ in self._entity_map: continue self._entity_map[entity_type.__name__] = True entity_type.__table__.create(self.engine) return self.session.query(*entity_types) def post(self, *entities): """ Insert new `entities` into the database. :param entities: a list of new entities. """ session = self.session for entity in entities: assert isinstance(entity, Entity), 'Expected an entity based on BaseEntity or a declarative base entity.' session.add(entity) session.commit() session.refresh(entity) def get(self, entity_type, key): """ Get an entity of type *entity_type*. :param entity_type: the class reference of the entities being searched :param key: the lookup key """ return self.query(entity_type).get(key) def get_all(self, entity_type): """ Get all entities of type *entity_type*. :param entity_type: the class reference of the entities being searched """ return self.query(entity_type).all()PKX MHYb passerine/db/fixture.py""" :Author: Juti Noppornpitak .. warning:: Not in used. """ import re from imagination.loader import Loader from passerine.exception import LoadedFixtureException try: from passerine.centre import services except ImportError as exception: pass class Fixture(object): """ Foundation of the council .. note:: this must be used at most once. .. warning:: this class is not tested. """ def __init__(self, repository): self.__repository = repository self.__loaded_kinds = {} self.__fixture_map = {} self.__graphs = {} self.__re_proxy = re.compile('^proxy/(?P[^/]+)/(?P.+)$') def set(self, kind, fixtures): """ Define the fixtures. :param kind: a string represent the kind :type kind: unicode|str :param fixtures: the data dictionary keyed by the alias :type fixtures: dict .. code-block:: python fixture = Fixture() fixture.set( 'council.security.model.Provider', { 'ldap': { 'name': 'ldap' } } ) fixture.set( 'council.user.model.User', { 'admin': { 'name': 'Juti Noppornpitak' } } ) fixture.set( 'council.security.model.Credential', { 'shiroyuki': { 'login': 'admin', 'user': 'proxy/council.user.model.User/admin', 'provider': 'proxy/council.security.model.Provider/ldap' } } ) """ self.__fixture_map[kind] = fixtures @property def db(self): return self.__repository def load(self): for kind in self.__fixture_map.keys(): self.load_fixtures(kind) def load_fixtures(self, kind): if kind in self.__loaded_kinds: return loader = Loader(kind) fixtures = self.__fixture_map[kind] self.__loaded_kinds[kind] = loader self.__graphs[kind] = [] for id, fixture in fixtures.iteritems(): fixture = self._prepare_fixture(fixture) entity = loader.package(**fixture) self.db.post(entity) self.db.session.refresh(entity) fixtures[id] = entity self.__graphs[kind].append(entity) def _prepare_fixture(self, fixture): for key, value in fixture.iteritems(): proxy_matches = self.__re_proxy.search(value)\ if (isinstance(value, unicode) or isinstance(value, str))\ else None if not proxy_matches: continue settings = proxy_matches.groupdict() self.load_fixtures(settings['kind']) fixture[key] = self.__fixture_map[settings['kind']][settings['alias']] return fixture PKX MH'$passerine/db/mochi.py""" Mochi Non-relational Database Library ##################################### :Author: Juti Noppornpitak This library is designed to work like SQLite but be compatible with MongoDB instructions (and PyMongo interfaces). """ import codecs import json class Mochi(dict): def __init__(self, location=None): self.__location = location with codecs.open(self.__location, 'r', 'utf-8') as fp: db_map = json.load(fp) for id in db_map: self[id] = Database(db_map[id]) class Database(dict): def __init__(self, initial_data_map={}): for id in initial_data_map: self[id] = Collection(initial_data_map[id]) class Collection(dict): def insert(self, data_set={}): pass def remove(self, criteria={}): pass def update(self, criteria={}, update_instructions={}, upsert=False): pass PKX MH passerine/db/driver/registrar.pyPKX MHpasserine/db/driver/__init__.pyPKX MH--!!"passerine/db/driver/mongodriver.pyimport re from pymongo import MongoClient from passerine.db.driver.interface import DriverInterface, QueryIteration, QuerySequence, DialectInterface from passerine.db.entity import Index from passerine.db.expression import Criteria, ExpressionOperand, ExpressionType, InvalidExpressionError from passerine.db.metadata.helper import EntityMetadataHelper class UnsupportedExpressionError(InvalidExpressionError): """ MongoDB-specific Unsupported Expression Error This is due to that the expression may be unsafe (e.g., 1 = 2) or result in unnecessary complex computation (e.g., e.mobile_phone = e.home_phone). """ class Dialect(DialectInterface): _OP_IN = '$in' _operand_map = {} def __init__(self): self._regex_ppath_delimiter = re.compile('\.') def get_native_operand(self, generic_operand): if not self._operand_map: self._operand_map.update({ ExpressionOperand.OP_EQ: None, ExpressionOperand.OP_NE: '$ne', ExpressionOperand.OP_GE: '$gte', ExpressionOperand.OP_GT: '$gt', ExpressionOperand.OP_LE: '$lte', ExpressionOperand.OP_LT: '$lt', ExpressionOperand.OP_IN: self._OP_IN, ExpressionOperand.OP_NOT_IN: '$nin', ExpressionOperand.OP_SQL_LIKE: '$regex' }) generic_operand = generic_operand.lower() if generic_operand not in self._operand_map: raise RuntimeError('{} operand is not supported for this engine.'.format(generic_operand)) return self._operand_map[generic_operand] def process_non_join_conditions(self, alias_to_conditions_map, definition_map, left, right, operand): if left.kind == right.kind: raise UnsupportedExpressionError('The given criteria contains an expression whose both sides of expression are of the same type (e.g., property path, primitive value and parameter). Unfortunately, the expression is not supported by the backend datastore.') # Re-reference the pointers (to improve the readability). property_side = None constrain_side = None if left.kind == ExpressionType.IS_PROPERTY_PATH: property_side = left constrain_side = right else: property_side = right constrain_side = left alias = property_side.alias property_path = ''.join(self._regex_ppath_delimiter.split(property_side.original)[1:]) constrain_value = constrain_side.value if constrain_side.kind == ExpressionType.IS_PARAMETER: parameter_name = constrain_side.alias constrain_value = definition_map[parameter_name] if alias not in alias_to_conditions_map: alias_to_conditions_map[alias] = {} # Replace the primary key if property_path == 'id': property_path = '_id' native_query = { property_path: constrain_value } if operand: native_query = { property_path: { operand: constrain_value } } alias_to_conditions_map[alias].update(native_query) def process_join_conditions(self, alias_to_conditions_map, alias, join_config, parent_alias): joined_keys = [document['_id'] for document in join_config['result_list']] current_native_query = alias_to_conditions_map[alias] parent_native_query = alias_to_conditions_map[join_config['parent_alias']] parent_native_query[join_config['property_path']] = { self._OP_IN: joined_keys } def get_iterating_constrains(self, query): option_map = {} if query._force_loading: option_map['_force_loading'] = query._force_loading # applicable only to MongoDB if query._order_by: option_map['sort'] = query._order_by if query._offset and query._offset > 0: option_map['skip'] = self._offset if query._limit and query._limit > 0: option_map['limit'] = query._limit return option_map class Driver(DriverInterface): def __init__(self, config, dialect = Dialect()): super(Driver, self).__init__(config, dialect) if isinstance(self.config, dict): if 'name' not in self.config: raise ValueError('Please specify the name of the database.') self.database_name = self.config['name'] del self.config['name'] return # "config" is an URL. matches = re.search('^mongodb://[^/]+/(?P[^\?]+)', config) if not matches: raise ValueError('Please specify the name of the database.') self.database_name = matches.groupdict()['database_name'] def connect(self): if self.client: return config = self.config if isinstance(config, dict): self.client = MongoClient(**config) # as a config map return self.client = MongoClient(config) # as an URL def disconnect(self): self.client.disconnect() self.client = None def db(self, name=None): if not self.client: self.connect() return self.client[name or self.database_name] def collections(self): return self.db().collections def collection(self, name): return self.db()[name] def insert(self, collection_name, data): api = self.collection(collection_name) return api.insert(data) def update(self, collection_name, criteria, new): api = self.collection(collection_name) return api.update(criteria, new) def remove(self, collection_name, criteria): api = self.collection(collection_name) return api.remove(criteria) def find_one(self, collection_name, criteria, fields=None): api = self.collection(collection_name) if fields: return api.find_one(criteria, fields) return api.find_one(criteria) def find(self, collection_name, criteria, fields=None): """ Find the data sets with the native API. :param collection_name: the name of the collection :param criteria: the criteria compatible with the native API. :param fields: the list of required fields. """ api = self.collection(collection_name) if fields: return api.find(criteria, fields) return api.find(criteria) def query(self, metadata, query, iterating_constrains): """ Find the data sets with :class:`passerine.db.query.Criteria`. :param metadata: the metadata of the target collection / repository :type metadata: passerine.db.metadata.entity.EntityMetadata :param query: the native query :param iterating_constrains: the iterating constrains """ collection_name = metadata.collection_name force_loading = iterating_constrains['_force_loading'] if '_force_loading' in iterating_constrains else False cursor = self.find(collection_name, query) if not force_loading and 'limit' in iterating_constrains and iterating_constrains['limit'] != 1: cursor = self.find(collection_name, query, fields=[]) for constrain in iterating_constrains: if '_' == constrain[0]: continue cursor.__getattribute__(constrain)(iterating_constrains[constrain]) return [data for data in cursor] def indice(self): return [index for index in self.collection('system.indexes').find()] # MongoDB-specific operation def ensure_index(self, collection_name, index, force_index): options = { 'background': (not force_index) } order_list = index.to_list() if isinstance(index, Index) else index if isinstance(order_list, list): indexed_field_list = ['{}_{}'.format(field, order) for field, order in order_list] indexed_field_list.sort() options['index_identifier'] = '-'.join(indexed_field_list) self.collection(collection_name).ensure_index(order_list, **options) def drop(self, collection_name): self.collection(collection_name).drop() def drop_indexes(self, collection_name): self.collection(collection_name).drop_indexes() def index_count(self): return self.total_row_count('system.indexes') def total_row_count(self, collection_name): return self.collection(collection_name).count()PKX MH o passerine/db/driver/interface.pyclass QueryIteration(object): """ Driver Query Iteration This is a metadata class representing an iteration in complex queries. :param str alias: the alias of the rewritten target :param dict native_query: the native query for a specific engine .. note:: Internal use only """ def __init__(self, alias, native_query): self.alias = alias self.native_query = native_query class QuerySequence(object): """ Driver Query Sequence The collection represents the sequence of sub queries. """ def __init__(self): self._iterations = [] def add(self, iteration): """ Append the the iteration :param passerine.db.driver.interface.QueryIteration iteration: the query iteration """ self._iterations.append(iteration) def each(self): """ Get the sequence iterator. """ for iteration in self._iterations: yield iteration class DialectInterface(object): """ Dialect interface It is used to translate a generic query into a native query. """ def get_native_operand(self, generic_operand): """ Translate a generic operand into a corresponding native operand. :param generic_operand: a generic operand :return: a native operand :rtype: str """ raise NotImplemented('Please define the operand transformation.') def get_alias_to_native_query_map(self, query): """ Retrieve a map from alias to native query. :param passerine.db.query.Query: the query object :rtype: dict """ expression_set = query.criteria.get_analyzed_version() alias_to_conditions_map = {} # Process the non-join conditions for expression in expression_set.expressions: operand = self.get_native_operand(expression.operand) left = expression.left right = expression.right self.process_non_join_conditions( alias_to_conditions_map, query.definition_map, left, right, operand ) # Handling the join conditions for alias in alias_to_conditions_map: join_config = query.join_map[alias] if not join_config['result_list']: continue parent_alias = join_config['parent_alias'] # "No parent alias" indicates that this is not a join query. Ignore the rest. if not parent_alias: continue self.process_join_conditions( alias_to_conditions_map, alias, join_config, parent_alias ) return alias_to_conditions_map def get_iterating_constrains(self, query): """ Retrieve the query constrains. :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented('Define the native constrains.') def process_join_conditions(self, alias_to_conditions_map, alias, join_config, parent_alias): """ Process the join conditions. :param dict alias_to_conditions_map: a alias-to-conditions map :param dict join_config: a join config map :param str alias: an alias of the given join map :param str parent_alias: the parent alias of the given join map :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented('Generate a query without join conditions.') def process_non_join_conditions(self, alias_to_conditions_map, definition_map, left, right, operand): """ Process the non-join conditions. :param dict alias_to_conditions_map: a alias-to-conditions map :param dict definition_map: a parameter-to-value map :param passerine.db.expression.ExpressionPart left: the left expression :param passerine.db.expression.ExpressionPart right: the right expression :param operand: the native operand :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented('Add join conditions.') class DriverInterface(object): """ The abstract driver interface :param dict config: the configuration used to initialize the database connection / client :param passerine.db.driver.interface.DialectInterface dialect: the corresponding dialect """ def __init__(self, config, dialect): self._config = config self._client = None self._database_name = None self._dialect = dialect @property def config(self): """ Driver configuration """ return self._config @config.setter def config(self, value): self._config = value @property def dialect(self): """ Driver dialect """ return self._dialect @dialect.setter def dialect(self, value): self._dialect = value @property def client(self): """ Driver Connection / Client """ return self._client @client.setter def client(self, client): self._client = client @property def database_name(self): """ The name of provisioned database """ return self._database_name @database_name.setter def database_name(self, database_name): self._database_name = database_name def db(self, name): """ Low-level Database-class API :return: the low-level database-class API :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented() def collection(self, name): """ Low-level Collection-class API :return: the low-level collection-class API :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented() def insert(self, collection_name, data): """ Low-level insert function :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented() def connect(self, config): """ Connect the client to the server. :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented() def disconnect(self): """ Disconnect the client. :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented() def indice(self): """ Retrieve the indice. :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented() def index_count(self): """ Retrieve the number of indexes. :raise NotImplemented: only if the interface is not overridden. """ raise NotImplemented()PKX MH !passerine/db/driver/riakdriver.pytry: from riak import RiakClient except ImportError as exception: pass # work in progress from passerine.db.driver.interface import DriverInterface class Driver(DriverInterface): def db(self, name): return None def collection(self, name): return self._client.bucket(name) def insert(self, collection_name, data): riak_object = self.collection(collection_name).new(data=inserted_data) riak_object.store() return riak_object.key def update(self, criteria_or_id, updated_data): # Note: criteria_or_id can only be a key due to backend limitation. riak_object = self.get(criteria_or_id) if not riak_object: raise ObjectNotFoundError('Unable to update the object.') for property_name in updated_data: riak_object.data[property_name] = updated_data[property_name] riak_object.store() def delete(self, criteria_or_id): # Note: criteria_or_id can only be a key due to backend limitation. riak_object = self.get(criteria_or_id) if not riak_object: return False riak_object.delete() return True def connect(self, config): self.client = RiakClient(config) def disconnect(self): raise NotImplemented()PKX MH\passerine/db/metadata/helper.pyfrom passerine.db.metadata.entity import EntityMetadata class EntityMetadataHelper(object): """ Entity Metadata Helper """ @staticmethod def imprint(cls, collection_name, indexes): """ Imprint the entity metadata to the class (type) :param cls: the entity class :type cls: type :param collection_name: the name of the collection (known as table, bucket etc.) :type collection_name: str :param indexes: the list of indexes :type indexes: list """ metadata = EntityMetadata() metadata.cls = cls metadata.collection_name = collection_name metadata.index_list = indexes cls.__tdbm__ = metadata @staticmethod def extract(cls): """ Extract the metadata of the given class :param cls: the entity class :type cls: type :rtype: passerine.db.metadata.entity.EntityMetadata """ return cls.__tdbm__ @staticmethod def hasMetadata(cls): """ Check if the given class *cls* has a metadata :param cls: the entity class :type cls: type :rtype: bool """ return '__tdbm__' in dir(cls) PKX MH!passerine/db/metadata/__init__.pyPKX MHX]passerine/db/metadata/entity.pyclass ReadOnlyEntityMetadataException(Exception): pass class EntityMetadata(object): """ Entity Metadata """ def __init__(self): self._locked = True # the metadata is read-only. self._cls = None self._collection_name = None self._index_list = [] self._relational_map = {} @property def cls(self): """ Entity Class """ return self._cls @cls.setter def cls(self, value): self._cls = value @property def collection_name(self): """ Collection / Bucket / Table Name """ return self._collection_name @collection_name.setter def collection_name(self, value): if self._collection_name and self._locked: raise ReadOnlyEntityMetadataException('The class metadata is read-only.') self._collection_name = value @property def relational_map(self): """ Relational Map """ return self._relational_map @relational_map.setter def relational_map(self, value): if self._relational_map and self._locked: raise ReadOnlyEntityMetadataException('The class metadata is read-only.') self._relational_map = value @property def index_list(self): """ Index List """ return self._index_list @index_list.setter def index_list(self, value): if self._index_list and self._locked: raise ReadOnlyEntityMetadataException('The class metadata is read-only.') self._index_list = value PKX MHpasserine/decorator/__init__.pyPKX MH+??passerine/decorator/common.py""" :Author: Juti Noppornpitak This package contains decorators for common use. """ import inspect from passerine.exception import * class BaseDecoratorForCallableObject(object): """ Base decorator based from an example at http://www.artima.com/weblogs/viewpost.jsp?thread=240808. """ def __init__(self, reference): """ On the initialization of the given ``function``. """ self._reference = reference def reference(self): return self._reference def __call__(self, *args, **kwargs): """ On the execution of the function. """ pass def make_singleton_class(class_reference, *args, **kwargs): """ Make the given class a singleton class. *class_reference* is a reference to a class type, not an instance of a class. *args* and *kwargs* are parameters used to instantiate a singleton instance. To use this, suppose we have a class called ``DummyClass`` and later instantiate a variable ``dummy_instnace`` as an instance of class ``DummyClass``. ``class_reference`` will be ``DummyClass``, not ``dummy_instance``. Note that this method is not for direct use. Always use ``@singleton`` or ``@singleton_with``. """ # Name of the attribute that store the singleton instance singleton_attr_name = '_singleton_instance' # The statice method to get the singleton instance of the reference class @staticmethod def instance(): """ Get a singleton instance. .. note:: This class is capable to act as a singleton class by invoking this method. """ return class_reference._singleton_instance # Intercept if the class has already been a singleton class. if singleton_attr_name in dir(class_reference): raise SingletonInitializationException( 'The attribute _singleton_instance is already assigned as instance of %s.'\ % type(class_reference._singleton_instance) ) # Instantiate an instance for a singleton class. class_reference._singleton_instance = class_reference(*args, **kwargs) class_reference.instance = instance return class_reference def singleton_with(*args, **kwargs): """ Decorator to make a class to be a singleton class with given parameters for the constructor. Please note that this decorator always requires parameters. Not giving one may result errors. Additionally, it is designed to solve the problem where the first parameter is a class reference. For normal usage, please use `@singleton` instead. Example:: # Declaration class MyAdapter(AdapterClass): def broadcast(self): print "Hello, world." @singleton_with(MyAdapter) class MyClass(ParentClass): def __init__(self, adapter): self.adapter = adapter() def take_action(self): self.adapter.broadcast() # Executing MyClass.instance().take_action() # expecting the message on the console. The end result is that the console will show the number from 1 to 10. """ # Only use the closure to handle the instatiation of the singleton of the instance. def inner_decorator(class_reference): return make_singleton_class(class_reference, *args, **kwargs) return inner_decorator def singleton(*args, **kwargs): """ Decorator to make a class to be a singleton class. This decorator is designed to be able to take parameters for the construction of the singleton instance. Please note that this decorator doesn't support the first parameter as a class reference. If you are using that way, please try to use ``@singleton_with`` instead. Example:: # Declaration @singleton class MyFirstClass(ParentClass): def __init__(self): self.number = 0 def call(self): self.number += 1 echo self.number # Or @singleton(20) class MySecondClass(ParentClass): def __init__(self, init_number): self.number = init_number def call(self): self.number += 1 echo self.number # Executing for i in range(10): MyFirstClass.instance().call() # Expecting 1-10 to be printed on the console. for i in range(10): MySecondClass.instance().call() # Expecting 11-20 to be printed on the console. The end result is that the console will show the number from 1 to 10. """ # Get the first parameter. first_param = args[0] # If the first parameter is really a reference to a class, then instantiate # the singleton instance. if len(args) == 1 and inspect.isclass(first_param) and isinstance(first_param, type): class_reference = first_param return make_singleton_class(class_reference) # Otherwise, use the closure to handle the parameter. def inner_decorator(class_reference): return make_singleton_class(class_reference, *args, **kwargs) return inner_decorator PKX MHܽbbpasserine/data/__init__.py""" :Author: Juti Noppornpitak Built-in Service Providers for Data Analysis and Transmission """ PKX MH[B  passerine/data/serializer.pyfrom passerine.decorator.common import singleton @singleton class ArraySerializer(object): def __init__(self, max_depth=2): self._max_depth = max_depth self._primitive_types = [] self._string_types = [] def set_max_depth(self, max_depth): self._max_depth = max_depth def encode(self, data, stack_depth=0): if not isinstance(data, object): raise TypeError('The provided data must be an object') returnee = {} for name in dir(data): if name[0] == '_': continue value = data.__getattribute__(name) if callable(value): continue if value and not self._is_primitive_type(value): if self._max_depth and stack_depth >= self._max_depth: value = value.encode('utf8', 'ignore') else: value = self.encode(value, stack_depth + 1) returnee[name] = value return returnee def _is_primitive_type(self, value): if not self._primitive_types: self._primitive_types = self.default_primitive_types() return type(value) in self._primitive_types def _is_string(self, value): if not self._string_types: self._string_types = self.default_string_types() return type(value) in self._string_types def default_string_types(self, value): try: return [str, unicode] except NameError as exception: return [str] def default_primitive_types(self): try: return [int, float, str, list, dict, tuple, set, bool, unicode, long] except NameError as exception: return [int, float, str, list, dict, tuple, set, bool]PKX MH69))passerine/data/exception.pyclass DirectoryException(Exception): passPKX MH7 passerine/data/base.pyfrom mimetypes import guess_type as get_type from os import path as p from imagination.helper import retrieve_module_path from passerine.exception import * module_path_map = {} def resolve_file_path(file_path): if ':' not in file_path: return file_path module_name, relative_path = file_path.split(':') if module_name in module_path_map: module_path = module_path_map[module_name] else: module_path = retrieve_module_path(module_name) module_path_map[module_name] = module_path return p.join(module_path, relative_path) class ResourceEntity(object): """ Static resource entity representing the real static resource which is already loaded to the memory. :param path: the path to the static resource. .. note:: This is for internal use only. """ DEFAULT_INDEX_FILE = 'index.html' def __init__(self, path, cacheable=False): accessible_path = resolve_file_path(path) self._is_originally_dir = p.isdir(accessible_path) if self._is_originally_dir: accessible_path = p.join(accessible_path, ResourceEntity.DEFAULT_INDEX_FILE) self._path = accessible_path self._content = None self._cacheable = cacheable self._type = get_type(accessible_path) self._type = self.kind[0] @property def original_request_path(self): return self._original_request_path @original_request_path.setter def original_request_path(self, value): self._original_request_path = value @property def is_originally_dir(self): return self._is_originally_dir @is_originally_dir.setter def is_originally_dir(self, value): self._is_originally_dir = value @property def kind(self): return self._type @property def path(self): return self._path @property def exists(self): return p.exists(self.path) @property def content(self): """ Get the content of the entity. """ if self._content: return self._content if not self.exists: return None with open(self.path) as f: self._content = f.read() f.close() return self._content @content.setter def content(self, new_content): """ Set the content of the entity. :param `new_content`: the new content """ self._content = new_content @property def cacheable(self): return self._cacheable class ResourceServiceMiddleware(object): def __init__(self, *intercepting_mimetypes): self._intercepting_mimetypes = intercepting_mimetypes def expect(self, entity): return isinstance(entity, ResourceEntity)\ and entity.kind in self._intercepting_mimetypes def execute(self, data): raise FutureFeatureException('This method must be implemented.')PKX MH|{passerine/data/compressor.pyfrom re import search, sub, split from passerine.data.base import ResourceServiceMiddleware class CSSCompressor(ResourceServiceMiddleware): def __init__(self): ResourceServiceMiddleware.__init__(self, 'text/css') def execute(self, data): content = sub('\n+', ' ', data.content) # Get rid of comment blocks raw_blocks = split('\*\/', content) blocks = [sub('\/\*.*$', '', block) for block in raw_blocks] content = (''.join(blocks)).strip() # Get rid of unnecessary whitespaces per line. lines = [] raw_lines = split('\n', content) for line in raw_lines: line = line.strip() lines.append(line) content = ' '.join(lines) # Get rid of unnecessary whitespaces as a whole. content = sub('\s*>\s*', ' > ', content) content = sub('\s*\+\s*', ' + ', content) content = sub('\s*~\s*', ' ~ ', content) content = sub('\s*:\s*', ':', content) content = sub('\s*;\s*', ';', content) content = sub('\s*\{\s*', '{', content) content = sub('\s*\}\s*', '}', content) content = sub('\s*,\s*', ',', content) content = sub(' +', ' ', content) data.content = content return dataPK#MH^- )passerine-1.3.0.dist-info/DESCRIPTION.rstUNKNOWN PK#MH!'passerine-1.3.0.dist-info/metadata.json{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries", "Topic :: Database"], "extensions": {"python.details": {"contacts": [{"email": "juti_n@yahoo.co.jp", "name": "Juti Noppornpitak", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/shiroyuki/passerine"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "license": "MIT", "metadata_version": "2.0", "name": "passerine", "run_requires": [{"requires": ["pymongo"]}], "summary": "A generic object relational mapper (ORM) and data abstraction layer (DAL) primarily designed for NoSQL databases.", "version": "1.3.0"}PK#MH! 'passerine-1.3.0.dist-info/top_level.txtpasserine PK#MH}\\passerine-1.3.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py3-none-any PK#MHr;CC"passerine-1.3.0.dist-info/METADATAMetadata-Version: 2.0 Name: passerine Version: 1.3.0 Summary: A generic object relational mapper (ORM) and data abstraction layer (DAL) primarily designed for NoSQL databases. Home-page: https://github.com/shiroyuki/passerine Author: Juti Noppornpitak Author-email: juti_n@yahoo.co.jp License: MIT Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Database Requires-Dist: pymongo UNKNOWN PK#MHl passerine-1.3.0.dist-info/RECORDpasserine/__init__.py,sha256=zoaFYnDE6fTf7K5vo9d_vvxWyOad3OP1NXBFNCmfmZI,96 passerine/common.py,sha256=fyibtHGmMpGdJuwt3zOz-_7lOXX5lc0HGF1ZG3up5sQ,2347 passerine/exception.py,sha256=ltbD4UyGAPHXvgSZtkXSfdJdzziFl5HwcM9OkAnxjA0,3155 passerine/graph.py,sha256=uZTdCLnFR-fqxu9jQHXcHyEX4KzMBTuyPmouxvY83nA,2219 passerine/data/__init__.py,sha256=X1GR9dhf5NzHlwMRK_COePrhUkr-YKfpwrgcNQWRRaQ,98 passerine/data/base.py,sha256=qwIvEJ-anKwPt_UoxoZ6kZ8VuLzKpx3O-uRMLbQHtP0,2956 passerine/data/compressor.py,sha256=-FCrf9IBgwU_TQnzob5hRmmESJXZi64I8TGKECIjlP4,1294 passerine/data/exception.py,sha256=hQcoMHzoV7lGkhMPb1-I2bjsEhfqg4QDKYOHeMNbVSA,41 passerine/data/serializer.py,sha256=fTpkeTIdyLuqx7TskxMVRQntLZ003j_TDHCfcyRPRpA,1801 passerine/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 passerine/db/common.py,sha256=4jC1VY1fu4iDBDyoLcJc3Q16_7jUVBDPyj2n49C3qKs,11257 passerine/db/entity.py,sha256=HDDMXEJFE25HypfuFcuelzvYU28Kn7oYVVyqOdS_Kus,6376 passerine/db/exception.py,sha256=kGe_SjOxbCwPb6aBK9RFsVa8eC6ZrnvctzXFNKSThRA,1872 passerine/db/expression.py,sha256=kxtrBBquxOoWr_m5Try6VPrGFgIBXz3xy-4YAXXMGz0,9068 passerine/db/fixture.py,sha256=jKhi6xQiaCy68iuy3ckl-RZxzcz3XPPPQ7bOwD6AMDs,3036 passerine/db/manager.py,sha256=oitoWT0TeYpT_hTHIYiP_reY-uXIwKPrI0adcsoh3T8,6317 passerine/db/mapper.py,sha256=ZmS5H83UFAXKpwJv-XK5ERZVNwAMwJiOEw3SW6KRJEk,10609 passerine/db/mochi.py,sha256=k0yRmDkIDbnd_G5pzWuFoiHMj3sNjMRb9QRyuuL_I9w,913 passerine/db/query.py,sha256=b5KlnkZT3cuuixsM3ZbfWjSOEoyqb-7B8V1BQQM8I1E,6542 passerine/db/repository.py,sha256=QR49YgJPe5r15CG2q495zopgxiAg2n0LVue7tXfhBfU,9482 passerine/db/session.py,sha256=_J-LL3dQHbEjtQsclUQrcRqAhlVjSy1y7-pXY3ghC6k,13976 passerine/db/uow.py,sha256=H_GIMPo5jDMungLieIT9QNG6WwR2NFrKNQJRDWTSUn8,25502 passerine/db/wrapper.py,sha256=bIIAHGtLMvFLWO3iCLV4aMihEfbnjJVgDnDQNOUd7mU,5151 passerine/db/driver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 passerine/db/driver/interface.py,sha256=UCEr3lfumOPbcTHQs8X3iI_N_QyNChTmYwdHTwTACoU,7104 passerine/db/driver/mongodriver.py,sha256=VXpBmgC8qRvR2JD8okp9--CkQf6Xq14pyC5E8Fs6FPE,8683 passerine/db/driver/registrar.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 passerine/db/driver/riakdriver.py,sha256=ZDW5F2zpNd5v68s4srrvbcHXCk8tWA86JHheEmmMtLY,1303 passerine/db/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 passerine/db/metadata/entity.py,sha256=M4R4brFo9Vgnml5o8mSzbw0LjsctK73cbV3IdQsE4Bs,1550 passerine/db/metadata/helper.py,sha256=5jEaTjW1H_RT5TkCAOh5n4aSfFsZyc_tvVZ9m8TtLWg,1279 passerine/decorator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 passerine/decorator/common.py,sha256=X32CWTC3ToQ5Swb2hTTCwvmzm1C_cU8AtmeqQAO9D2o,5183 passerine-1.3.0.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 passerine-1.3.0.dist-info/METADATA,sha256=hI9sTE5MGOXrgKL1CuUIUzrqL9NWLzOq5Cl9naR7q9s,835 passerine-1.3.0.dist-info/RECORD,, passerine-1.3.0.dist-info/WHEEL,sha256=zX7PHtH_7K-lEzyK75et0UBa3Bj8egCBMXe1M4gc6SU,92 passerine-1.3.0.dist-info/metadata.json,sha256=fa2F0FeLc7L4WJoGF9eTI3QtZfpGdDSXa8vSVN2rbHk,974 passerine-1.3.0.dist-info/top_level.txt,sha256=F8jMbBBM_zGb22MWyvdtHCsDUHxdZ_-FFfJ9qs0QaUk,10 PKX MH4irm``passerine/__init__.pyPKX MHw$S S passerine/exception.pyPKX MH+ +  passerine/common.pyPKX MH%\ܫvpasserine/graph.pyPKX MH[ۘ66Qpasserine/db/session.pyPKX MHvyVpasserine/db/query.pyPKX MHopasserine/db/__init__.pyPKX MHS* % %ppasserine/db/repository.pyPK"MHQKUWpasserine/db/manager.pyPKX MH#pq)q)9passerine/db/mapper.pyPKX MHPPpasserine/db/exception.pyPKX MHql#l#epasserine/db/expression.pyPKX MHW passerine/db/entity.pyPKX MHCgcc%passerine/db/uow.pyPKX MH:++passerine/db/common.pyPKX MHG!passerine/db/wrapper.pyPKX MHYb upasserine/db/fixture.pyPKX MH'$passerine/db/mochi.pyPKX MH Jpasserine/db/driver/registrar.pyPKX MHpasserine/db/driver/__init__.pyPKX MH--!!"passerine/db/driver/mongodriver.pyPKX MH o passerine/db/driver/interface.pyPKX MH !passerine/db/driver/riakdriver.pyPKX MH\Dpasserine/db/metadata/helper.pyPKX MH!passerine/db/metadata/__init__.pyPKX MHX]passerine/db/metadata/entity.pyPKX MH passerine/decorator/__init__.pyPKX MH+??G passerine/decorator/common.pyPKX MHܽbb4passerine/data/__init__.pyPKX MH[B  [5passerine/data/serializer.pyPKX MH69))<passerine/data/exception.pyPKX MH7 =passerine/data/base.pyPKX MH|{Hpasserine/data/compressor.pyPK#MH^- )Npasserine-1.3.0.dist-info/DESCRIPTION.rstPK#MH!'YNpasserine-1.3.0.dist-info/metadata.jsonPK#MH! 'lRpasserine-1.3.0.dist-info/top_level.txtPK#MH}\\Rpasserine-1.3.0.dist-info/WHEELPK#MHr;CC"TSpasserine-1.3.0.dist-info/METADATAPK#MHl Vpasserine-1.3.0.dist-info/RECORDPK''* c