import os import requests import json from dotenv import load_dotenv load_dotenv(override=True) class SuperOfficeClient: """A client for interacting with the SuperOffice REST API.""" def __init__(self): self.client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD") self.client_secret = os.getenv("SO_CLIENT_SECRET") self.refresh_token = os.getenv("SO_REFRESH_TOKEN") self.redirect_uri = os.getenv("SO_REDIRECT_URI", "http://localhost") self.env = os.getenv("SO_ENVIRONMENT", "sod") self.cust_id = os.getenv("SO_CONTEXT_IDENTIFIER", "Cust55774") # Fallback for your dev if not all([self.client_id, self.client_secret, self.refresh_token]): raise ValueError("SuperOffice credentials missing in .env file.") self.base_url = f"https://app-{self.env}.superoffice.com/{self.cust_id}/api/v1" self.access_token = self._refresh_access_token() if not self.access_token: raise Exception("Failed to authenticate with SuperOffice.") self.headers = { "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json", "Accept": "application/json" } print("✅ SuperOffice Client initialized and authenticated.") def _refresh_access_token(self): """Refreshes and returns a new access token.""" url = f"https://{self.env}.superoffice.com/login/common/oauth/tokens" data = { "grant_type": "refresh_token", "client_id": self.client_id, "client_secret": self.client_secret, "refresh_token": self.refresh_token, "redirect_uri": self.redirect_uri } try: resp = requests.post(url, data=data) resp.raise_for_status() return resp.json().get("access_token") except requests.exceptions.HTTPError as e: print(f"❌ Token Refresh Error: {e.response.text}") return None except Exception as e: print(f"❌ Connection Error during token refresh: {e}") return None def _get(self, endpoint): """Generic GET request.""" try: resp = requests.get(f"{self.base_url}/{endpoint}", headers=self.headers) resp.raise_for_status() return resp.json() except requests.exceptions.HTTPError as e: print(f"❌ API GET Error for {endpoint}: {e.response.text}") return None def _put(self, endpoint, payload): """Generic PUT request.""" try: resp = requests.put(f"{self.base_url}/{endpoint}", headers=self.headers, json=payload) resp.raise_for_status() return resp.json() except requests.exceptions.HTTPError as e: print(f"❌ API PUT Error for {endpoint}: {e.response.text}") return None def get_person(self, person_id): """Gets a single person by ID.""" return self._get(f"Person/{person_id}") def get_contact(self, contact_id): """Gets a single contact (company) by ID.""" return self._get(f"Contact/{contact_id}") def update_udfs(self, entity: str, entity_id: int, udf_payload: dict): """ Updates the UserDefinedFields for a given entity (Person or Contact). Args: entity (str): "Person" or "Contact". entity_id (int): The ID of the entity. udf_payload (dict): A dictionary of ProgId:Value pairs. """ endpoint = f"{entity}/{entity_id}" # 1. GET the full entity object existing_data = self._get(endpoint) if not existing_data: return False # Error is printed in _get # 2. Merge the UDF payload if "UserDefinedFields" not in existing_data: existing_data["UserDefinedFields"] = {} existing_data["UserDefinedFields"].update(udf_payload) # 3. PUT the full object back print(f"Updating {entity} {entity_id} with new UDFs...") result = self._put(endpoint, existing_data) if result: print(f"✅ Successfully updated {entity} {entity_id}") return True return False def search(self, query_string: str): """ Performs a search using OData syntax and handles pagination. Example: "Person?$select=personId&$filter=lastname eq 'Godelmann'" """ all_results = [] next_page_url = f"{self.base_url}/{query_string}" while next_page_url: try: resp = requests.get(next_page_url, headers=self.headers) resp.raise_for_status() data = resp.json() # Add the items from the current page all_results.extend(data.get('value', [])) # Check for the next page link next_page_url = data.get('next_page_url', None) except requests.exceptions.HTTPError as e: print(f"❌ API Search Error for {query_string}: {e.response.text}") return None return all_results