from __future__ import print_function import requests import json import frappe from six import iteritems, string_types ''' FrappeClient is a library that helps you connect with other frappe systems ''' class AuthError(Exception): pass class FrappeException(Exception): pass class FrappeClient(object): def __init__(self, url, username, password, verify=True): self.headers = dict(Accept='application/json') self.verify = verify self.session = requests.session() self.url = url self._login(username, password) def __enter__(self): return self def __exit__(self, *args, **kwargs): self.logout() def _login(self, username, password): '''Login/start a sesion. Called internally on init''' r = self.session.post(self.url, data={ 'cmd': 'login', 'usr': username, 'pwd': password }, verify=self.verify, headers=self.headers) if r.status_code==200 and r.json().get('message') == "Logged In": return r.json() else: print(r.text) raise AuthError def logout(self): '''Logout session''' self.session.get(self.url, params={ 'cmd': 'logout', }, verify=self.verify, headers=self.headers) def get_list(self, doctype, fields='"*"', filters=None, limit_start=0, limit_page_length=0): """Returns list of records of a particular type""" if not isinstance(fields, string_types): fields = json.dumps(fields) params = { "fields": fields, } if filters: params["filters"] = json.dumps(filters) if limit_page_length: params["limit_start"] = limit_start params["limit_page_length"] = limit_page_length res = self.session.get(self.url + "/api/resource/" + doctype, params=params, verify=self.verify, headers=self.headers) return self.post_process(res) def insert(self, doc): '''Insert a document to the remote server :param doc: A dict or Document object to be inserted remotely''' res = self.session.post(self.url + "/api/resource/" + doc.get("doctype"), data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers) return self.post_process(res) def insert_many(self, docs): '''Insert multiple documents to the remote server :param docs: List of dict or Document objects to be inserted in one request''' return self.post_request({ "cmd": "frappe.client.insert_many", "docs": frappe.as_json(docs) }) def update(self, doc): '''Update a remote document :param doc: dict or Document object to be updated remotely. `name` is mandatory for this''' url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name") res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers) return self.post_process(res) def bulk_update(self, docs): '''Bulk update documents remotely :param docs: List of dict or Document objects to be updated remotely (by `name`)''' return self.post_request({ "cmd": "frappe.client.bulk_update", "docs": frappe.as_json(docs) }) def delete(self, doctype, name): '''Delete remote document by name :param doctype: `doctype` to be deleted :param name: `name` of document to be deleted''' return self.post_request({ "cmd": "frappe.client.delete", "doctype": doctype, "name": name }) def submit(self, doc): '''Submit remote document :param doc: dict or Document object to be submitted remotely''' return self.post_request({ "cmd": "frappe.client.submit", "doc": frappe.as_json(doc) }) def get_value(self, doctype, fieldname=None, filters=None): '''Returns a value form a document :param doctype: DocType to be queried :param fieldname: Field to be returned (default `name`) :param filters: dict or string for identifying the record''' return self.get_request({ "cmd": "frappe.client.get_value", "doctype": doctype, "fieldname": fieldname or "name", "filters": frappe.as_json(filters) }) def set_value(self, doctype, docname, fieldname, value): '''Set a value in a remote document :param doctype: DocType of the document to be updated :param docname: name of the document to be updated :param fieldname: fieldname of the document to be updated :param value: value to be updated''' return self.post_request({ "cmd": "frappe.client.set_value", "doctype": doctype, "name": docname, "fieldname": fieldname, "value": value }) def cancel(self, doctype, name): '''Cancel a remote document :param doctype: DocType of the document to be cancelled :param name: name of the document to be cancelled''' return self.post_request({ "cmd": "frappe.client.cancel", "doctype": doctype, "name": name }) def get_doc(self, doctype, name="", filters=None, fields=None): '''Returns a single remote document :param doctype: DocType of the document to be returned :param name: (optional) `name` of the document to be returned :param filters: (optional) Filter by this dict if name is not set :param fields: (optional) Fields to be returned, will return everythign if not set''' params = {} if filters: params["filters"] = json.dumps(filters) if fields: params["fields"] = json.dumps(fields) res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name, params=params, verify=self.verify, headers=self.headers) return self.post_process(res) def rename_doc(self, doctype, old_name, new_name): '''Rename remote document :param doctype: DocType of the document to be renamed :param old_name: Current `name` of the document to be renamed :param new_name: New `name` to be set''' params = { "cmd": "frappe.client.rename_doc", "doctype": doctype, "old_name": old_name, "new_name": new_name } return self.post_request(params) def migrate_doctype(self, doctype, filters=None, update=None, verbose=1, exclude=None, preprocess=None): """Migrate records from another doctype""" meta = frappe.get_meta(doctype) tables = {} for df in meta.get_table_fields(): if verbose: print("getting " + df.options) tables[df.fieldname] = self.get_list(df.options, limit_page_length=999999) # get links if verbose: print("getting " + doctype) docs = self.get_list(doctype, limit_page_length=999999, filters=filters) # build - attach children to parents if tables: docs = [frappe._dict(doc) for doc in docs] docs_map = dict((doc.name, doc) for doc in docs) for fieldname in tables: for child in tables[fieldname]: child = frappe._dict(child) if child.parent in docs_map: docs_map[child.parent].setdefault(fieldname, []).append(child) if verbose: print("inserting " + doctype) for doc in docs: if exclude and doc["name"] in exclude: continue if preprocess: preprocess(doc) if not doc.get("owner"): doc["owner"] = "Administrator" if doctype != "User" and not frappe.db.exists("User", doc.get("owner")): frappe.get_doc({"doctype": "User", "email": doc.get("owner"), "first_name": doc.get("owner").split("@")[0] }).insert() if update: doc.update(update) doc["doctype"] = doctype new_doc = frappe.get_doc(doc) new_doc.insert() if not meta.istable: if doctype != "Communication": self.migrate_doctype("Communication", {"reference_doctype": doctype, "reference_name": doc["name"]}, update={"reference_name": new_doc.name}, verbose=0) if doctype != "File": self.migrate_doctype("File", {"attached_to_doctype": doctype, "attached_to_name": doc["name"]}, update={"attached_to_name": new_doc.name}, verbose=0) def migrate_single(self, doctype): doc = self.get_doc(doctype, doctype) doc = frappe.get_doc(doc) # change modified so that there is no error doc.modified = frappe.db.get_single_value(doctype, "modified") frappe.get_doc(doc).insert() def get_api(self, method, params={}): res = self.session.get(self.url + "/api/method/" + method + "/", params=params, verify=self.verify, headers=self.headers) return self.post_process(res) def post_api(self, method, params={}): res = self.session.post(self.url + "/api/method/" + method + "/", params=params, verify=self.verify, headers=self.headers) return self.post_process(res) def get_request(self, params): res = self.session.get(self.url, params=self.preprocess(params), verify=self.verify, headers=self.headers) res = self.post_process(res) return res def post_request(self, data): res = self.session.post(self.url, data=self.preprocess(data), verify=self.verify, headers=self.headers) res = self.post_process(res) return res def preprocess(self, params): """convert dicts, lists to json""" for key, value in iteritems(params): if isinstance(value, (dict, list)): params[key] = json.dumps(value) return params def post_process(self, response): try: rjson = response.json() except ValueError: print(response.text) raise if rjson and ("exc" in rjson) and rjson["exc"]: raise FrappeException(rjson["exc"]) if 'message' in rjson: return rjson['message'] elif 'data' in rjson: return rjson['data'] else: return None class FrappeOAuth2Client(FrappeClient): def __init__(self, url, access_token, verify=True): self.access_token = access_token self.headers = { "Authorization": "Bearer " + access_token, "content-type": "application/x-www-form-urlencoded" } self.verify = verify self.session = OAuth2Session(self.headers) self.url = url def get_request(self, params): res = requests.get(self.url, params=self.preprocess(params), headers=self.headers, verify=self.verify) res = self.post_process(res) return res def post_request(self, data): res = requests.post(self.url, data=self.preprocess(data), headers=self.headers, verify=self.verify) res = self.post_process(res) return res class OAuth2Session(): def __init__(self, headers): self.headers = headers def get(self, url, params, verify): res = requests.get(url, params=params, headers=self.headers, verify=verify) return res def post(self, url, data, verify): res = requests.post(url, data=data, headers=self.headers, verify=verify) return res def put(self, url, data, verify): res = requests.put(url, data=data, headers=self.headers, verify=verify) return res