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

378 строки
11 KiB

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