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

datasource_plugin.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. ## @package lodel.plugin.datasource_plugin Datasource plugins management module
  2. #
  3. # It contains the base classes for all the datasource plugins that could be added to Lodel
  4. from lodel.plugin.plugins import Plugin
  5. from lodel.plugin.exceptions import PluginError, PluginTypeError, LodelScriptError, DatasourcePluginError
  6. from lodel.validator.validator import Validator
  7. from lodel.exceptions import LodelException, LodelExceptions, LodelFatalError, DataNoneValid, FieldValidationError
  8. ## @brief The plugin type that is used in the global settings of Lodel
  9. _glob_typename = 'datasource'
  10. ## @brief Main abstract class from which the plugins' datasource classes must inherit.
  11. class AbstractDatasource(object):
  12. ## @brief Trigger LodelFatalError when abtract method called
  13. # @throw LodelFatalError if there is an attempt to instanciate an object from this class
  14. @staticmethod
  15. def _abs_err():
  16. raise LodelFatalError("This method is abstract and HAVE TO be \
  17. reimplemented by plugin datasource child class")
  18. ##
  19. # @param *conn_args
  20. # @param **conn_kwargs
  21. def __init__(self, *conn_args, **conn_kwargs):
  22. self._abs_err()
  23. ## @brief Provides a new uniq numeric ID
  24. # @param emcomp LeObject subclass (not instance) : defines against which objects type the id should be unique
  25. # @return int
  26. def new_numeric_id(self, emcomp):
  27. self._abs_err()
  28. ## @brief Returns a selection of documents from the datasource
  29. # @param target Emclass : class of the documents
  30. # @param field_list list : fields to get from the datasource
  31. # @param filters list : List of filters
  32. # @param rel_filters list : List of relational filters (default value : None)
  33. # @param order list : List of column to order. ex: order = [('title', 'ASC'),] (default value : None)
  34. # @param group list : List of tupple representing the column to group together. ex: group = [('title', 'ASC'),] (default value : None)
  35. # @param limit int : Number of records to be returned (default value None)
  36. # @param offset int: used with limit to choose the start record (default value : 0)
  37. # @param instanciate bool : If true, the records are returned as instances, else they are returned as dict (default value : True)
  38. # @return list
  39. def select(self, target, field_list, filters, rel_filters=None, order=None, group=None, limit=None, offset=0,
  40. instanciate=True):
  41. self._abs_err()
  42. ## @brief Deletes records according to given filters
  43. # @param target Emclass : class of the record to delete
  44. # @param filters list : List of filters
  45. # @param relational_filters list : List of relational filters
  46. # @return int : number of deleted records
  47. def delete(self, target, filters, relational_filters):
  48. self._abs_err()
  49. ## @brief updates records according to given filters
  50. # @param target Emclass : class of the object to insert
  51. # @param filters list : List of filters
  52. # @param relational_filters list : List of relational filters
  53. # @param upd_datas dict : datas to update (new values)
  54. # @return int : Number of updated records
  55. def update(self, target, filters, relational_filters, upd_datas):
  56. self._abs_err()
  57. ## @brief Inserts a record in a given collection
  58. # @param target Emclass : class of the object to insert
  59. # @param new_datas dict : datas to insert
  60. # @return the inserted uid
  61. def insert(self, target, new_datas):
  62. self._abs_err()
  63. ## @brief Inserts a list of records in a given collection
  64. # @param target Emclass : class of the objects inserted
  65. # @param datas_list list : list of dict
  66. # @return list : list of the inserted records' ids
  67. def insert_multi(self, target, datas_list):
  68. self._abs_err()
  69. ## @brief Represents a Datasource plugin
  70. #
  71. # It will provide an access to a data collection to LeAPI (i.e. database connector, API ...).
  72. #
  73. # It provides the methods needed to initialize the datasource attribute in LeAPI LeObject child
  74. # classes (see @ref leapi.leobject.LeObject._init_datasources() )
  75. #
  76. # @note For the moment implementation is done with a retro-compatibilities priority and not with a convenience priority.
  77. # @todo Refactor and rewrite lodel2 datasource handling
  78. # @todo Write abstract classes for Datasource and MigrationHandler !!!
  79. class DatasourcePlugin(Plugin):
  80. _type_conf_name = _glob_typename
  81. ## @brief Stores confspecs indicating where DatasourcePlugin list is stored
  82. _plist_confspecs = {
  83. 'section': 'lodel2',
  84. 'key': 'datasource_connectors',
  85. 'default': 'dummy_datasource',
  86. 'validator': Validator(
  87. 'custom_list', none_is_valid = False,
  88. validator_name = 'plugin', validator_kwargs = {
  89. 'ptype': _glob_typename,
  90. 'none_is_valid': False})
  91. }
  92. ##
  93. # @param name str : plugin's name
  94. # @see plugins.Plugin
  95. def __init__(self, name):
  96. super().__init__(name)
  97. self.__datasource_cls = None
  98. ## @brief Returns an accessor to the datasource class
  99. # @return A python datasource class
  100. # @throw DatasourcePluginError if the plugin's datasource class is not a child of
  101. # @ref lodel.plugin.datasource_plugin.AbstractDatasource
  102. def datasource_cls(self):
  103. if self.__datasource_cls is None:
  104. self.__datasource_cls = self.loader_module().Datasource
  105. if not issubclass(self.__datasource_cls, AbstractDatasource):
  106. raise DatasourcePluginError("The datasource class of the \
  107. '%s' plugin is not a child class of \
  108. lodel.plugin.datasource_plugin.AbstractDatasource" % (self.name))
  109. return self.__datasource_cls
  110. ## @brief Returns an accessor to migration handler class
  111. # @return A python migration handler class
  112. def migration_handler_cls(self):
  113. return self.loader_module().migration_handler_class()
  114. ## @brief Returns an initialized Datasource instance
  115. # @param ds_name str : The name of the datasource to instanciate
  116. # @param ro bool : indicates if it will be in read only mode, else it will be in write only mode
  117. # @return A properly initialized Datasource instance
  118. @classmethod
  119. def init_datasource(cls, ds_name, ro):
  120. plugin_name, ds_identifier = cls.plugin_name(ds_name, ro)
  121. ds_conf = cls._get_ds_connection_conf(ds_identifier, plugin_name)
  122. ds_cls = cls.get_datasource(plugin_name)
  123. return ds_cls(**ds_conf)
  124. ## @brief Returns an initialized MigrationHandler instance
  125. # @param ds_name str : The datasource name
  126. # @return A properly initialized MigrationHandler instance
  127. # @throw PluginError if a read only datasource instance was given to the migration handler.
  128. @classmethod
  129. def init_migration_handler(cls, ds_name):
  130. plugin_name, ds_identifier = cls.plugin_name(ds_name, False)
  131. ds_conf = cls._get_ds_connection_conf(ds_identifier, plugin_name)
  132. mh_cls = cls.get_migration_handler(plugin_name)
  133. if 'read_only' in ds_conf:
  134. if ds_conf['read_only']:
  135. raise PluginError("A read only datasource was given to \
  136. migration handler !!!")
  137. del(ds_conf['read_only'])
  138. return mh_cls(**ds_conf)
  139. ## @brief Given a datasource name returns a DatasourcePlugin name
  140. # @param ds_name str : datasource name
  141. # @param ro bool : if true consider the datasource as readonly
  142. # @return a DatasourcePlugin name
  143. # @throw DatasourcePluginError if the given datasource is unknown or not configured, or if there is a conflict in its "read-only" property (between the instance and the settings).
  144. # @throw SettingsError if there are misconfigured datasource settings.
  145. @staticmethod
  146. def plugin_name(ds_name, ro):
  147. from lodel.settings import Settings
  148. # fetching connection identifier given datasource name
  149. try:
  150. ds_identifier = getattr(Settings.datasources, ds_name)
  151. except (NameError, AttributeError):
  152. raise DatasourcePluginError("Unknown or unconfigured datasource \
  153. '%s'" % ds_name)
  154. # fetching read_only flag
  155. try:
  156. read_only = getattr(ds_identifier, 'read_only')
  157. except (NameError, AttributeError):
  158. raise SettingsError("Malformed datasource configuration for '%s' \
  159. : missing read_only key" % ds_name)
  160. # fetching datasource identifier
  161. try:
  162. ds_identifier = getattr(ds_identifier, 'identifier')
  163. except (NameError,AttributeError) as e:
  164. raise SettingsError("Malformed datasource configuration for '%s' \
  165. : missing identifier key" % ds_name)
  166. # settings and ro arg consistency check
  167. if read_only and not ro:
  168. raise DatasourcePluginError("ro argument was set to False but \
  169. True found in settings for datasource '%s'" % ds_name)
  170. res = ds_identifier.split('.')
  171. if len(res) != 2:
  172. raise SettingsError("expected value for identifier is like \
  173. DS_PLUGIN_NAME.DS_INSTANCE_NAME. But got %s" % ds_identifier)
  174. return res
  175. ## @brief Returns a datasource's configuration
  176. # @param ds_identifier str : datasource name
  177. # @param ds_plugin_name : datasource plugin name
  178. # @return a dict containing datasource initialisation options
  179. # @throw DatasourcePluginError if a datasource plugin or instance cannot be found
  180. @staticmethod
  181. def _get_ds_connection_conf(ds_identifier,ds_plugin_name):
  182. from lodel.settings import Settings
  183. if ds_plugin_name not in Settings.datasource._fields:
  184. msg = "Unknown or unconfigured datasource plugin %s"
  185. msg %= ds_plugin_name
  186. raise DatasourcePluginError(msg)
  187. ds_conf = getattr(Settings.datasource, ds_plugin_name)
  188. if ds_identifier not in ds_conf._fields:
  189. msg = "Unknown or unconfigured datasource instance %s"
  190. msg %= ds_identifier
  191. raise DatasourcePluginError(msg)
  192. ds_conf = getattr(ds_conf, ds_identifier)
  193. return {k: getattr(ds_conf,k) for k in ds_conf._fields }
  194. ## @brief Returns a DatasourcePlugin instance from a plugin's name
  195. # @param ds_name str : plugin name
  196. # @return DatasourcePlugin
  197. # @throw PluginError if no plugin named ds_name found (@see lodel.plugin.plugins.Plugin)
  198. # @throw PluginTypeError if ds_name ref to a plugin that is not a DatasourcePlugin
  199. @classmethod
  200. def get(cls, ds_name):
  201. pinstance = super().get(ds_name) # Will raise PluginError if bad name
  202. if not isinstance(pinstance, DatasourcePlugin):
  203. raise PluginTypeErrror("A name of a DatasourcePlugin was excepted \
  204. but %s is a %s" % (ds_name, pinstance.__class__.__name__))
  205. return pinstance
  206. ## @brief Returns a datasource class given a datasource name
  207. # @param ds_plugin_name str : datasource plugin name
  208. # @return Datasource class
  209. @classmethod
  210. def get_datasource(cls, ds_plugin_name):
  211. return cls.get(ds_plugin_name).datasource_cls()
  212. ## @brief Returns a migration handler class, given a plugin name
  213. # @param ds_plugin_name str : a datasource plugin name
  214. # @return MigrationHandler class
  215. @classmethod
  216. def get_migration_handler(cls, ds_plugin_name):
  217. return cls.get(ds_plugin_name).migration_handler_cls()
  218. ## @page lodel2_datasources Lodel2 datasources
  219. #
  220. # @par lodel2_datasources_intro Introduction
  221. # A single lodel2 website can interact with multiple datasources. This page
  222. # aims to describe configuration and organisation of datasources in lodel2.
  223. # Each object is attached to a datasource. This association is done in the
  224. # editorial model, in which the datasource is identified by its name.
  225. #
  226. # @par Datasources declaration
  227. # To define a datasource you have to write something like this in configuration file :
  228. # <pre>
  229. # [lodel2.datasources.DATASOURCE_NAME]
  230. # identifier = DATASOURCE_FAMILY.SOURCE_NAME
  231. # </pre>
  232. # See below for DATASOURCE_FAMILY & SOURCE_NAME
  233. #
  234. # @par Datasources plugins
  235. # Each datasource family is a plugin ( @ref plugin_doc "More informations on plugins" ).
  236. # For example mysql or a mongodb plugins. \n
  237. #
  238. # Here is the CONFSPEC variable templates for datasources plugin
  239. #<pre>
  240. #CONFSPEC = {
  241. # 'lodel2.datasource.example.*' : {
  242. # 'conf1' : VALIDATOR_OPTS,
  243. # 'conf2' : VALIDATOR_OPTS,
  244. # ...
  245. # }
  246. #}
  247. #</pre>
  248. #
  249. #MySQL example \n
  250. #<pre>
  251. #CONFSPEC = {
  252. # 'lodel2.datasource.mysql.*' : {
  253. # 'host': ( 'localhost',
  254. # Validator('host')),
  255. # 'db_name': ( 'lodel',
  256. # Validator('string')),
  257. # 'username': ( None,
  258. # Validator('string')),
  259. # 'password': ( None,
  260. # Validator('string')),
  261. # }
  262. #}
  263. #</pre>
  264. #
  265. # @par Configuration example
  266. # <pre>
  267. # [lodel2.datasources.main]
  268. # identifier = mysql.Core
  269. # [lodel2.datasources.revues_write]
  270. # identifier = mysql.Revues
  271. # [lodel2.datasources.revues_read]
  272. # identifier = mysql.Revues
  273. # [lodel2.datasources.annuaire_persons]
  274. # identifier = persons_web_api.example
  275. # ;
  276. # ; Then, in the editorial model you are able to use "main", "revues_write",
  277. # ; etc as datasource
  278. # ;
  279. # ; Here comes the datasources declarations
  280. # [lodel2.datasource.mysql.Core]
  281. # host = db.core.labocleo.org
  282. # db_name = core
  283. # username = foo
  284. # password = bar
  285. # ;
  286. # [lodel2.datasource.mysql.Revues]
  287. # host = revues.org
  288. # db_name = RO
  289. # username = foo
  290. # password = bar
  291. # ;
  292. # [lodel2.datasource.persons_web_api.example]
  293. # host = foo.bar
  294. # username = cleo
  295. #</pre>