Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

datasource_plugin.py 12KB

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