Exemplo n.º 1
0
    def __init__(self, *args, **kwargs):
        self.system_client = SystemAPIClient()

        self.settings = DjconnectwiseSettings().get_settings()

        if not self.CALLBACK_ID:
            raise NotImplementedError('CALLBACK_ID must be assigned a value')

        if not self.CALLBACK_TYPE:
            raise NotImplementedError('CALLBACK_TYPE must be assigned a value')
Exemplo n.º 2
0
    def __init__(
        self,
        company_id=None,
        server_url=None,
        client_id=None,
        api_public_key=None,
        api_private_key=None,
    ):
        if not company_id:
            company_id = settings.CONNECTWISE_CREDENTIALS['company_id']
        if not server_url:
            server_url = settings.CONNECTWISE_SERVER_URL
        if not client_id:
            client_id = settings.CONNECTWISE_CLIENTID
        if not api_public_key:
            api_public_key = settings.CONNECTWISE_CREDENTIALS['api_public_key']
        if not api_private_key:
            api_private_key = settings.CONNECTWISE_CREDENTIALS[
                'api_private_key'
            ]

        if not self.API:
            raise ValueError('API not specified')

        self.info_manager = CompanyInfoManager()
        self.company_id = company_id
        self.api_public_key = api_public_key
        self.api_private_key = api_private_key
        self.server_url = self.change_cw_cloud_url(server_url)
        self.client_id = client_id
        self.auth = (
            '{0}+{1}'.format(company_id, self.api_public_key),
            '{0}'.format(self.api_private_key),
        )

        self.request_settings = DjconnectwiseSettings().get_settings()
        self.timeout = self.request_settings['timeout']
        self.api_base_url = None  # This will be set to the base URL for this
        # particular API-
        # i.e. https://connectwise.example.com/v4_6_release/apis/3.0/service/

        self.build_api_base_url(force_fetch=False)
Exemplo n.º 3
0
class ConnectWiseAPIClient(object):
    API = None
    MAX_404_ATTEMPTS = 1

    def __init__(
        self,
        company_id=None,
        server_url=None,
        client_id=None,
        api_public_key=None,
        api_private_key=None,
    ):
        if not company_id:
            company_id = settings.CONNECTWISE_CREDENTIALS['company_id']
        if not server_url:
            server_url = settings.CONNECTWISE_SERVER_URL
        if not client_id:
            client_id = settings.CONNECTWISE_CLIENTID
        if not api_public_key:
            api_public_key = settings.CONNECTWISE_CREDENTIALS['api_public_key']
        if not api_private_key:
            api_private_key = settings.CONNECTWISE_CREDENTIALS[
                'api_private_key']

        if not self.API:
            raise ValueError('API not specified')

        self.info_manager = CompanyInfoManager()
        self.company_id = company_id
        self.api_public_key = api_public_key
        self.api_private_key = api_private_key
        self.server_url = self.change_cw_cloud_url(server_url)
        self.client_id = client_id
        self.auth = (
            '{0}+{1}'.format(company_id, self.api_public_key),
            '{0}'.format(self.api_private_key),
        )

        self.request_settings = DjconnectwiseSettings().get_settings()
        self.timeout = self.request_settings['timeout']
        self.api_base_url = None  # This will be set to the base URL for this
        # particular API-
        # i.e. https://connectwise.example.com/v4_6_release/apis/3.0/service/

        self.build_api_base_url(force_fetch=False)

    def _endpoint(self, path):
        return '{0}{1}'.format(self.api_base_url, path)

    def _log_failed(self, response):
        logger.error('Failed API call: {0} - {1} - {2}'.format(
            response.url, response.status_code, response.content))

    def _prepare_error_response(self, response):
        error = response.content.decode("utf-8")
        # decode the bytes encoded error to a string
        # error = error.args[0].decode("utf-8")
        error = error.replace('\r\n', '')
        messages = []

        try:
            error = json.loads(error)
            stripped_message = error.get('message').rstrip('.') if \
                error.get('message') else 'No message'
            primary_error_msg = '{}.'.format(stripped_message)
            if error.get('errors'):
                for error_message in error.get('errors'):
                    messages.append('{}.'.format(
                        error_message.get('message').rstrip('.')))

            messages = ' The error was: '.join(messages)

            msg = '{} {}'.format(primary_error_msg, messages)

        except json.decoder.JSONDecodeError:
            # JSON decoding failed
            msg = 'An error occurred: {} {}'.format(response.status_code,
                                                    error)
        except KeyError:
            # 'code' or 'message' was not found in the error
            msg = 'An error occurred: {} {}'.format(response.status_code,
                                                    error)
        return msg

    def build_api_base_url(self, force_fetch):
        api_codebase, codebase_updated = \
            self.info_manager.fetch_api_codebase(
                self.server_url, self.company_id, force_fetch=force_fetch
            )

        self.api_base_url = '{0}/{1}apis/3.0/{2}/'.format(
            self.server_url,
            api_codebase,
            self.API,
        )

        return codebase_updated

    def get_headers(self):
        headers = {}

        response_version = self.request_settings.get('response_version')
        if response_version:
            headers['Accept'] = \
                'application/vnd.connectwise.com+json; version={}' \
                .format(response_version)

        if self.client_id:
            headers['clientId'] = self.client_id

        return headers

    def fetch_resource(self,
                       endpoint_url,
                       params=None,
                       should_page=False,
                       retry_counter=None,
                       *args,
                       **kwargs):
        """
        Issue a GET request to the specified REST endpoint.

        retry_counter is a dict in the form {'count': 0} that is passed in
        to verify the number of attempts that were made.
        """
        @retry(stop_max_attempt_number=self.request_settings['max_attempts'],
               wait_exponential_multiplier=RETRY_WAIT_EXPONENTIAL_MULTAPPLIER,
               wait_exponential_max=RETRY_WAIT_EXPONENTIAL_MAX,
               retry_on_exception=retry_if_api_error)
        def _fetch_resource(endpoint_url,
                            params=None,
                            should_page=False,
                            retry_counter=None,
                            *args,
                            **kwargs):
            if not retry_counter:
                retry_counter = {'count': 0}
            retry_counter['count'] += 1

            if not params:
                params = {}

            try:
                endpoint = self._endpoint(endpoint_url)
                logger.debug('Making GET request to {}'.format(endpoint))

                conditions_str = ''
                conditions = kwargs.get('conditions')
                if conditions:
                    logger.debug('Conditions: {}'.format(conditions))
                    conditions_str = 'conditions={}'.format(
                        self.prepare_conditions(conditions))
                    # URL encode needed characters
                    conditions_str = conditions_str.replace("+", "%2B")
                    conditions_str = conditions_str.replace(" ", "+")

                if should_page:
                    params['pageSize'] = kwargs.get('page_size',
                                                    CW_RESPONSE_MAX_RECORDS)
                    params['page'] = kwargs.get('page', CW_DEFAULT_PAGE)
                    endpoint += "?pageSize={}&page={}".format(
                        params['pageSize'], params['page'])

                    endpoint += "&" + conditions_str
                else:
                    endpoint += "?" + conditions_str

                response = requests.get(
                    endpoint,
                    auth=self.auth,
                    timeout=self.timeout,
                    headers=self.get_headers(),
                )
                logger.info(" URL: {}".format(response.url))

            except requests.RequestException as e:
                logger.error('Request failed: GET {}: {}'.format(endpoint, e))
                raise ConnectWiseAPIError('{}'.format(e))

            if 200 <= response.status_code < 300:
                return response.json()

            elif response.status_code == 404:
                msg = 'Resource not found: {}'.format(response.url)
                logger.warning(msg)
                # If this is the first failure, try updating the
                # company info codebase value and let it be retried by the
                # @retry decorator.
                if retry_counter['count'] <= self.MAX_404_ATTEMPTS:
                    codebase_updated = self.build_api_base_url(
                        force_fetch=True)
                    if codebase_updated:
                        # Since the codebase was updated, it is worthwhile
                        # to try this request again. It could be that the 404
                        # was due to hosted ConnectWise changing the codebase
                        # URL recently. So raise ConnectWiseAPIError, which
                        # will cause the call to be retried.
                        logger.info('Codebase value has changed, so this '
                                    'request will be retried.')
                        raise ConnectWiseAPIError(response.content)
                raise ConnectWiseRecordNotFoundError(msg)

            elif 400 <= response.status_code < 499:
                self._log_failed(response)
                raise ConnectWiseAPIClientError(
                    self._prepare_error_response(response))
            elif response.status_code == 500:
                self._log_failed(response)
                raise ConnectWiseAPIServerError(
                    self._prepare_error_response(response))
            else:
                self._log_failed(response)
                raise ConnectWiseAPIError(
                    self._prepare_error_response(response))

        if not retry_counter:
            retry_counter = {'count': 0}
        return _fetch_resource(endpoint_url,
                               params=params,
                               should_page=should_page,
                               retry_counter=retry_counter,
                               *args,
                               **kwargs)

    def prepare_conditions(self, conditions):
        """
        From the given array of individual conditions, format the conditions
        URL parameter according to ConnectWise requirements.
        """
        return '({})'.format(' and '.join(conditions))

    def request(self, method, endpoint_url, body=None):
        """
        Issue the given type of request to the specified REST endpoint.
        """
        try:
            logger.debug('Making {} request to {}'.format(
                method, endpoint_url))
            response = requests.request(
                method,
                endpoint_url,
                json=body,
                auth=self.auth,
                timeout=self.timeout,
                headers=self.get_headers(),
            )
        except requests.RequestException as e:
            logger.error('Request failed: {} {}: {}'.format(
                method, endpoint_url, e))
            raise ConnectWiseAPIError('{}'.format(e))

        if response.status_code == 204:  # No content
            return None
        elif 200 <= response.status_code < 300:
            return response.json()
        elif response.status_code == 404:
            msg = 'Resource not found: {}'.format(response.url)
            logger.warning(msg)
            raise ConnectWiseRecordNotFoundError(msg)
        elif 400 <= response.status_code < 499:
            self._log_failed(response)
            raise ConnectWiseAPIClientError(
                self._prepare_error_response(response))
        elif response.status_code == 500:
            self._log_failed(response)
            raise ConnectWiseAPIServerError(
                self._prepare_error_response(response))
        else:
            self._log_failed(response)
            raise ConnectWiseAPIError(response)

    def change_cw_cloud_url(self, server_url):
        """
        Replace the user-facing CW CLoud URLs with the API URLs.

        i.e. https://na.myconnectwise.net becomes
        https://api-na.myconnectwise.net

        See https://developer.connectwise.com/Products/Manage/Developer_Guide#Authentication  # noqa
        """
        url = urlparse(server_url)
        if url.netloc not in CW_CLOUD_URLS:
            # Don't change anything, just return.
            return server_url

        return url._replace(netloc=CW_CLOUD_URLS[url.netloc]).geturl()
Exemplo n.º 4
0
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # This can be replaced with a single instantiation of an OrderedDict
        # using kwargs in Python 3.6. But we need Python 3.5 compatibility for
        # now.
        # See https://www.python.org/dev/peps/pep-0468/.
        synchronizers = (
            ('member', sync.MemberSynchronizer, _('Member')),
            ('board', sync.BoardSynchronizer, _('Board')),
            ('team', sync.TeamSynchronizer, _('Team')),
            ('board_status', sync.BoardStatusSynchronizer, _('Board Status')),
            ('priority', sync.PrioritySynchronizer, _('Priority')),
            ('project_status', sync.ProjectStatusSynchronizer,
             _('Project Status')),
            ('project_type', sync.ProjectTypeSynchronizer, _('Project Type')),
            ('project', sync.ProjectSynchronizer, _('Project')),
            ('project_phase', sync.ProjectPhaseSynchronizer,
             _('Project Phase')),
            ('territory', sync.TerritorySynchronizer, _('Territory')),
            ('company_status', sync.CompanyStatusSynchronizer,
             _('Company Status')),
            ('company_type', sync.CompanyTypeSynchronizer, _('Company Type')),
            ('company', sync.CompanySynchronizer, _('Company')),
            ('location', sync.LocationSynchronizer, _('Location')),
            ('opportunity_status', sync.OpportunityStatusSynchronizer,
             _('Opportunity Status')),
            ('opportunity_stage', sync.OpportunityStageSynchronizer,
             _('Opportunity Stage')),
            ('opportunity_type', sync.OpportunityTypeSynchronizer,
             _('Opportunity Type')),
            ('sales_probability', sync.SalesProbabilitySynchronizer,
             _('Sales Probability')),
            ('opportunity', sync.OpportunitySynchronizer, _('Opportunity')),
            ('holiday_list', sync.HolidayListSynchronizer, _('Holiday List')),
            ('holiday', sync.HolidaySynchronizer, _('Holiday')),
            ('calendar', sync.CalendarSynchronizer, _('Calendar')),
            ('company_other', sync.MyCompanyOtherSynchronizer,
             _('Company Other')),
            ('sla', sync.SLASynchronizer, _('Sla')),
            ('sla_priority', sync.SLAPrioritySynchronizer, _('Sla Priority')),
            ('type', sync.TypeSynchronizer, _('Type')),
            ('sub_type', sync.SubTypeSynchronizer, _('Sub Type')),
            ('item', sync.ItemSynchronizer, _('Item')),
            ('ticket', sync.ServiceTicketSynchronizer, _('Ticket')),
            ('project_ticket', sync.ProjectTicketSynchronizer,
             _('Project Ticket')),
            ('work_type', sync.WorkTypeSynchronizer, _('Work Type')),
            ('work_role', sync.WorkRoleSynchronizer, _('Work Role')),
            ('agreement', sync.AgreementSynchronizer, _('Agreement')),
            ('activity_status', sync.ActivityStatusSynchronizer,
             _('Activity Status')),
            ('activity_type', sync.ActivityTypeSynchronizer,
             _('Activity Type')),
            ('activity', sync.ActivitySynchronizer, _('Activity')),
            ('schedule_type', sync.ScheduleTypeSynchronizer,
             _('Schedule Type')),
            ('schedule_status', sync.ScheduleStatusSynchronizer,
             _('Schedule Status')),
            ('schedule_entry', sync.ScheduleEntriesSynchronizer,
             _('Schedule Entry')),
        )

        settings = DjconnectwiseSettings().get_settings()
        if settings['sync_time_and_note_entries']:
            synchronizers = synchronizers + (
                ('service_note', sync.ServiceNoteSynchronizer,
                 _('Service Note')),
                ('opportunity_note', sync.OpportunityNoteSynchronizer,
                 _('Opportunity Note')),
                ('time_entry', sync.TimeEntrySynchronizer, _('Time Entry')))

        self.synchronizer_map = OrderedDict()
        for name, synchronizer, obj_name in synchronizers:
            self.synchronizer_map[name] = (synchronizer, obj_name)
Exemplo n.º 5
0
 def __init__(self):
     super().__init__()
     self.system_client = SystemAPIClient()
     self.settings = DjconnectwiseSettings().get_settings()