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.
 
 
 
 
 
 

330 lines
10 KiB

  1. from __future__ import print_function
  2. import requests
  3. import json
  4. import frappe
  5. from six import iteritems, string_types
  6. '''
  7. FrappeClient is a library that helps you connect with other frappe systems
  8. '''
  9. class AuthError(Exception):
  10. pass
  11. class FrappeException(Exception):
  12. pass
  13. class FrappeClient(object):
  14. def __init__(self, url, username, password, verify=True):
  15. self.headers = dict(Accept='application/json')
  16. self.verify = verify
  17. self.session = requests.session()
  18. self.url = url
  19. self._login(username, password)
  20. def __enter__(self):
  21. return self
  22. def __exit__(self, *args, **kwargs):
  23. self.logout()
  24. def _login(self, username, password):
  25. '''Login/start a sesion. Called internally on init'''
  26. r = self.session.post(self.url, data={
  27. 'cmd': 'login',
  28. 'usr': username,
  29. 'pwd': password
  30. }, verify=self.verify, headers=self.headers)
  31. if r.status_code==200 and r.json().get('message') == "Logged In":
  32. return r.json()
  33. else:
  34. print(r.text)
  35. raise AuthError
  36. def logout(self):
  37. '''Logout session'''
  38. self.session.get(self.url, params={
  39. 'cmd': 'logout',
  40. }, verify=self.verify, headers=self.headers)
  41. def get_list(self, doctype, fields='"*"', filters=None, limit_start=0, limit_page_length=0):
  42. """Returns list of records of a particular type"""
  43. if not isinstance(fields, string_types):
  44. fields = json.dumps(fields)
  45. params = {
  46. "fields": fields,
  47. }
  48. if filters:
  49. params["filters"] = json.dumps(filters)
  50. if limit_page_length:
  51. params["limit_start"] = limit_start
  52. params["limit_page_length"] = limit_page_length
  53. res = self.session.get(self.url + "/api/resource/" + doctype, params=params, verify=self.verify, headers=self.headers)
  54. return self.post_process(res)
  55. def insert(self, doc):
  56. '''Insert a document to the remote server
  57. :param doc: A dict or Document object to be inserted remotely'''
  58. res = self.session.post(self.url + "/api/resource/" + doc.get("doctype"),
  59. data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers)
  60. return self.post_process(res)
  61. def insert_many(self, docs):
  62. '''Insert multiple documents to the remote server
  63. :param docs: List of dict or Document objects to be inserted in one request'''
  64. return self.post_request({
  65. "cmd": "frappe.client.insert_many",
  66. "docs": frappe.as_json(docs)
  67. })
  68. def update(self, doc):
  69. '''Update a remote document
  70. :param doc: dict or Document object to be updated remotely. `name` is mandatory for this'''
  71. url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name")
  72. res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers)
  73. return self.post_process(res)
  74. def bulk_update(self, docs):
  75. '''Bulk update documents remotely
  76. :param docs: List of dict or Document objects to be updated remotely (by `name`)'''
  77. return self.post_request({
  78. "cmd": "frappe.client.bulk_update",
  79. "docs": frappe.as_json(docs)
  80. })
  81. def delete(self, doctype, name):
  82. '''Delete remote document by name
  83. :param doctype: `doctype` to be deleted
  84. :param name: `name` of document to be deleted'''
  85. return self.post_request({
  86. "cmd": "frappe.client.delete",
  87. "doctype": doctype,
  88. "name": name
  89. })
  90. def submit(self, doc):
  91. '''Submit remote document
  92. :param doc: dict or Document object to be submitted remotely'''
  93. return self.post_request({
  94. "cmd": "frappe.client.submit",
  95. "doc": frappe.as_json(doc)
  96. })
  97. def get_value(self, doctype, fieldname=None, filters=None):
  98. '''Returns a value form a document
  99. :param doctype: DocType to be queried
  100. :param fieldname: Field to be returned (default `name`)
  101. :param filters: dict or string for identifying the record'''
  102. return self.get_request({
  103. "cmd": "frappe.client.get_value",
  104. "doctype": doctype,
  105. "fieldname": fieldname or "name",
  106. "filters": frappe.as_json(filters)
  107. })
  108. def set_value(self, doctype, docname, fieldname, value):
  109. '''Set a value in a remote document
  110. :param doctype: DocType of the document to be updated
  111. :param docname: name of the document to be updated
  112. :param fieldname: fieldname of the document to be updated
  113. :param value: value to be updated'''
  114. return self.post_request({
  115. "cmd": "frappe.client.set_value",
  116. "doctype": doctype,
  117. "name": docname,
  118. "fieldname": fieldname,
  119. "value": value
  120. })
  121. def cancel(self, doctype, name):
  122. '''Cancel a remote document
  123. :param doctype: DocType of the document to be cancelled
  124. :param name: name of the document to be cancelled'''
  125. return self.post_request({
  126. "cmd": "frappe.client.cancel",
  127. "doctype": doctype,
  128. "name": name
  129. })
  130. def get_doc(self, doctype, name="", filters=None, fields=None):
  131. '''Returns a single remote document
  132. :param doctype: DocType of the document to be returned
  133. :param name: (optional) `name` of the document to be returned
  134. :param filters: (optional) Filter by this dict if name is not set
  135. :param fields: (optional) Fields to be returned, will return everythign if not set'''
  136. params = {}
  137. if filters:
  138. params["filters"] = json.dumps(filters)
  139. if fields:
  140. params["fields"] = json.dumps(fields)
  141. res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name,
  142. params=params, verify=self.verify, headers=self.headers)
  143. return self.post_process(res)
  144. def rename_doc(self, doctype, old_name, new_name):
  145. '''Rename remote document
  146. :param doctype: DocType of the document to be renamed
  147. :param old_name: Current `name` of the document to be renamed
  148. :param new_name: New `name` to be set'''
  149. params = {
  150. "cmd": "frappe.client.rename_doc",
  151. "doctype": doctype,
  152. "old_name": old_name,
  153. "new_name": new_name
  154. }
  155. return self.post_request(params)
  156. def migrate_doctype(self, doctype, filters=None, update=None, verbose=1, exclude=None, preprocess=None):
  157. """Migrate records from another doctype"""
  158. meta = frappe.get_meta(doctype)
  159. tables = {}
  160. for df in meta.get_table_fields():
  161. if verbose: print("getting " + df.options)
  162. tables[df.fieldname] = self.get_list(df.options, limit_page_length=999999)
  163. # get links
  164. if verbose: print("getting " + doctype)
  165. docs = self.get_list(doctype, limit_page_length=999999, filters=filters)
  166. # build - attach children to parents
  167. if tables:
  168. docs = [frappe._dict(doc) for doc in docs]
  169. docs_map = dict((doc.name, doc) for doc in docs)
  170. for fieldname in tables:
  171. for child in tables[fieldname]:
  172. child = frappe._dict(child)
  173. if child.parent in docs_map:
  174. docs_map[child.parent].setdefault(fieldname, []).append(child)
  175. if verbose: print("inserting " + doctype)
  176. for doc in docs:
  177. if exclude and doc["name"] in exclude:
  178. continue
  179. if preprocess:
  180. preprocess(doc)
  181. if not doc.get("owner"):
  182. doc["owner"] = "Administrator"
  183. if doctype != "User" and not frappe.db.exists("User", doc.get("owner")):
  184. frappe.get_doc({"doctype": "User", "email": doc.get("owner"),
  185. "first_name": doc.get("owner").split("@")[0] }).insert()
  186. if update:
  187. doc.update(update)
  188. doc["doctype"] = doctype
  189. new_doc = frappe.get_doc(doc)
  190. new_doc.insert()
  191. if not meta.istable:
  192. if doctype != "Communication":
  193. self.migrate_doctype("Communication", {"reference_doctype": doctype, "reference_name": doc["name"]},
  194. update={"reference_name": new_doc.name}, verbose=0)
  195. if doctype != "File":
  196. self.migrate_doctype("File", {"attached_to_doctype": doctype,
  197. "attached_to_name": doc["name"]}, update={"attached_to_name": new_doc.name}, verbose=0)
  198. def migrate_single(self, doctype):
  199. doc = self.get_doc(doctype, doctype)
  200. doc = frappe.get_doc(doc)
  201. # change modified so that there is no error
  202. doc.modified = frappe.db.get_single_value(doctype, "modified")
  203. frappe.get_doc(doc).insert()
  204. def get_api(self, method, params={}):
  205. res = self.session.get(self.url + "/api/method/" + method + "/",
  206. params=params, verify=self.verify, headers=self.headers)
  207. return self.post_process(res)
  208. def post_api(self, method, params={}):
  209. res = self.session.post(self.url + "/api/method/" + method + "/",
  210. params=params, verify=self.verify, headers=self.headers)
  211. return self.post_process(res)
  212. def get_request(self, params):
  213. res = self.session.get(self.url, params=self.preprocess(params), verify=self.verify, headers=self.headers)
  214. res = self.post_process(res)
  215. return res
  216. def post_request(self, data):
  217. res = self.session.post(self.url, data=self.preprocess(data), verify=self.verify, headers=self.headers)
  218. res = self.post_process(res)
  219. return res
  220. def preprocess(self, params):
  221. """convert dicts, lists to json"""
  222. for key, value in iteritems(params):
  223. if isinstance(value, (dict, list)):
  224. params[key] = json.dumps(value)
  225. return params
  226. def post_process(self, response):
  227. try:
  228. rjson = response.json()
  229. except ValueError:
  230. print(response.text)
  231. raise
  232. if rjson and ("exc" in rjson) and rjson["exc"]:
  233. raise FrappeException(rjson["exc"])
  234. if 'message' in rjson:
  235. return rjson['message']
  236. elif 'data' in rjson:
  237. return rjson['data']
  238. else:
  239. return None
  240. class FrappeOAuth2Client(FrappeClient):
  241. def __init__(self, url, access_token, verify=True):
  242. self.access_token = access_token
  243. self.headers = {
  244. "Authorization": "Bearer " + access_token,
  245. "content-type": "application/x-www-form-urlencoded"
  246. }
  247. self.verify = verify
  248. self.session = OAuth2Session(self.headers)
  249. self.url = url
  250. def get_request(self, params):
  251. res = requests.get(self.url, params=self.preprocess(params), headers=self.headers, verify=self.verify)
  252. res = self.post_process(res)
  253. return res
  254. def post_request(self, data):
  255. res = requests.post(self.url, data=self.preprocess(data), headers=self.headers, verify=self.verify)
  256. res = self.post_process(res)
  257. return res
  258. class OAuth2Session():
  259. def __init__(self, headers):
  260. self.headers = headers
  261. def get(self, url, params, verify):
  262. res = requests.get(url, params=params, headers=self.headers, verify=verify)
  263. return res
  264. def post(self, url, data, verify):
  265. res = requests.post(url, data=data, headers=self.headers, verify=verify)
  266. return res
  267. def put(self, url, data, verify):
  268. res = requests.put(url, data=data, headers=self.headers, verify=verify)
  269. return res