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 14KB

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