"""Main module.""" import json import requests import logging from typing import List, Optional, Dict, Union from bs4 import BeautifulSoup log = logging.getLogger(__name__) class APIError (BaseException): ... class CallFailed(APIError): ... class APIv4: def __init__(self, base_url: str): self.base_url = base_url self.api_url = self.base_url + "/civicrm/ajax/api4" self.session = None def login(self, user: str, password: str): self.session = requests.Session() ### Perform login post self.session.cookies.update({"has_js": "1"}) postdata = { "name": user, "pass": password, "form_id": "user_login_block", } res = self.session.post(self.base_url, data=postdata) if (not res.ok): log.error("Login failed. Code: {}. Result: {}".format(res.status_code, res.text)) # we should raise an error here. self.session = None return # session now has the correct session cookie to do authenticated requests # these headers seem to be required to pass CSRF self.session.headers.update({ "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/json;charset=utf-8", }) log.info("Successfully logged into CRM.") def _call(self, objName: str, action: str, params: Optional[Dict] = None) -> requests.Response: """ Low level, minimal processing call to the API. """ if (self.session is None): log.error("Need to login first.") raise APIError("Need to login first.") url = self.api_url+'/'+objName+'/'+action if params: result = self.session.post(url, params={'params':json.dumps(params)}, data="") else: result = self.session.post(url, data="") if (not result.ok): raise CallFailed("Call returned non-2xx: {} {}".format(result.status_code, result.text)) return result.json() def call_values(self, objName: str, action: str, params: Optional[Dict] = None) -> Optional[List]: """A quick wrapper to just return the values of an API call""" result = self._call(objName, action, params) if result and ("values" in result): return result["values"] return None def get_fields(self, objName: str) -> List: """ Return a list of fields on the target object type. """ return self.call_values(objName, "getFields") def get_actions(self, objName: str) -> List: """ Return a list of possible actions for a given object type. """ return self.call_values(objName, "getActions") def get_entities(self, detailed: bool = False) -> Union[List[str], List[Dict]]: """Return a list of possible entities.""" ent = self.call_values("Entity", "get") if detailed: return ent return [x["name"] for x in ent] def get(self, objName: str, where: Optional[List]=None, limit: Optional[int]=None, select=Optional[List[str]]) -> List: """Get objects based on a where clause, optional limit and optional field list.""" params = {} if limit is not None: params["limit"] = limit if where is not None: params["where"] = where if columns is not None and columns: params["select"] = select return self.call_values(objName, "get", params=params) def create(self, objName: str, values: Dict) -> Dict: """Create an object using the values specified, and return the newly created object.""" result = self.call_values(objName, "create", params={"values": values}) if result: return result[0] return {} def delete(self, objName: str, where: List) -> int: """Delete objects with the specified where clauses and return the count..""" result = self._call(objName, "delete", params={"where": where}) if "count" in result: return result["count"] return 0 def replace(self, objName: str, records: List[Dict], where: List) -> List: ... def update(self, objName: str, fields: Dict, where: List) -> List: """Update is an upsert operation.""" ...