25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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