暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

datasource_plugin.py 14KB

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