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

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