コード例 #1
0
 def __init__(self,
              api_key: str,
              on_error: Callable[[str], None],
              flush_queue_size: int,
              flush_interval: timedelta,
              request_timeout: timedelta,
              min_id_length: Optional[int],
              events_endpoint: Optional[str],
              identification_endpoint: Optional[str]) -> None:
     self._api_key = api_key
     self._request_timeout = request_timeout
     self._min_id_length = min_id_length
     self._on_error = on_error
     self._queue: queue.Queue = AsyncConsumer.create_queue()
     self._endpoints = {
         "events": Endpoint(url=events_endpoint or "https://api.amplitude.com/2/httpapi", is_json=True),
         "identification": Endpoint(url=identification_endpoint or "https://api.amplitude.com/identify",
                                    is_json=False),
     }
     self._session = Session()
     self._consumer = AsyncConsumer(message_queue=self._queue,
                                    do_upload=self._upload_batch,
                                    flush_queue_size=flush_queue_size,
                                    flush_interval=flush_interval)
     atexit.register(self.shutdown)
     self._consumer.start()
コード例 #2
0
 def __init__(self, api_endpoint: str, api_key: str, flush_queue_size: int,
              flush_interval: timedelta, request_timeout: timedelta,
              omit_values: bool, retry_options: IterativelyRetryOptions,
              on_error: Callable[[str], None]) -> None:
     self._api_endpoint = api_endpoint
     self._api_key = api_key
     self._request_timeout = request_timeout
     self._omit_values = omit_values
     self._retry_options = retry_options
     self._on_error = on_error
     self._queue: queue.Queue = AsyncConsumer.create_queue()
     self._session = Session()
     self._consumer = AsyncConsumer(self._queue,
                                    do_upload=self._upload_batch,
                                    flush_queue_size=flush_queue_size,
                                    flush_interval=flush_interval)
     atexit.register(self.shutdown)
     self._consumer.start()
コード例 #3
0
 def __init__(
     self,
     base_url: str,
     api_key: str,
     flush_queue_size: int,
     flush_interval: timedelta,
     request_timeout: timedelta,
     logger: Logger,
 ) -> None:
     self._api_key = api_key
     self._request_timeout = request_timeout
     self._queue: queue.Queue = AsyncConsumer.create_queue()
     base_url = base_url.rstrip("/")
     self._user_track_url = f'{base_url}/users/track'
     self._session = Session()
     self._logger = logger
     self._consumer = AsyncConsumer(message_queue=self._queue,
                                    do_upload=self._upload_batch,
                                    flush_queue_size=flush_queue_size,
                                    flush_interval=flush_interval)
     atexit.register(self.shutdown)
     self._consumer.start()
コード例 #4
0
class IterativelyClient:
    def __init__(self, api_endpoint: str, api_key: str, flush_queue_size: int,
                 flush_interval: timedelta, request_timeout: timedelta,
                 omit_values: bool, retry_options: IterativelyRetryOptions,
                 on_error: Callable[[str], None]) -> None:
        self._api_endpoint = api_endpoint
        self._api_key = api_key
        self._request_timeout = request_timeout
        self._omit_values = omit_values
        self._retry_options = retry_options
        self._on_error = on_error
        self._queue: queue.Queue = AsyncConsumer.create_queue()
        self._session = Session()
        self._consumer = AsyncConsumer(self._queue,
                                       do_upload=self._upload_batch,
                                       flush_queue_size=flush_queue_size,
                                       flush_interval=flush_interval)
        atexit.register(self.shutdown)
        self._consumer.start()

    def track(self,
              track_type: TrackType,
              event: Optional[Event] = None,
              properties: Optional[Properties] = None,
              validation: Optional[ValidationResponse] = None) -> None:
        date_sent = datetime.utcnow().strftime(
            '%Y-%m-%dT%H:%M:%S.%f')[:-3] + "Z"
        model = {
            "type": track_type.value,
            "dateSent": date_sent,
            "properties": {},
            "valid": True,
            "validation": {
                "details": "",
            },
        }

        if event is not None:
            model['eventName'] = event.name
            if event.id is not None:
                model['eventId'] = event.id
            if event.version is not None:
                model['eventSchemaVersion'] = event.version

        if properties is not None:
            if self._omit_values:
                model['properties'] = {k: None for k in properties.to_json()}
            else:
                model['properties'] = properties.to_json()

        if validation is not None:
            model['valid'] = validation.valid
            if not self._omit_values and validation.message is not None:
                model['validation']['details'] = validation.message

        self._enqueue(AsyncConsumerMessage("events", model))

    def _upload_batch(self, batch: List[AsyncConsumerMessage],
                      stop_event: threading.Event) -> None:
        data = {
            'objects': [message.data for message in batch],
        }
        try:
            self._send_request(data, stop_event)
        except Exception as e:
            self._on_error(str(e))

    def _send_request(self, data: Any, stop_event: threading.Event) -> None:
        need_retry = self._post_request(data)
        if not need_retry:
            return
        for delay in backoff(
                start=self._retry_options.delay_initial.total_seconds(),
                stop=self._retry_options.delay_maximum.total_seconds(),
                count=self._retry_options.max_retries - 1,
                factor=2.0,
                jitter=1.0):
            if stop_event.wait(delay):
                return

            need_retry = self._post_request(data)
            if not need_retry:
                return
        raise Exception("Failed to upload events. Maximum attempts exceeded.")

    def _post_request(self, data: Any) -> bool:
        try:
            response = self._session.post(
                self._api_endpoint,
                json=data,
                headers={'Authorization': 'Bearer ' + self._api_key},
                timeout=self._request_timeout.total_seconds())
        except requests.ConnectionError:
            return True
        except requests.Timeout:
            return True
        except Exception as e:
            raise Exception(f"A unhandled exception occurred. ({e}).")

        if 200 <= response.status_code < 300:
            return False
        if 500 <= response.status_code < 600:
            return True
        if response.status_code == 429:
            return True
        raise Exception(
            f"Upload failed due to unhandled HTTP error ({response.status_code})."
        )

    def shutdown(self) -> None:
        self._consumer.shutdown()

    def _enqueue(self, message: AsyncConsumerMessage) -> None:
        try:
            self._queue.put(message)
        except queue.Full:
            self._on_error("async queue is full")

    def flush(self) -> None:
        self._consumer.flush()
コード例 #5
0
class AmplitudeClient:
    def __init__(self,
                 api_key: str,
                 on_error: Callable[[str], None],
                 flush_queue_size: int,
                 flush_interval: timedelta,
                 request_timeout: timedelta,
                 min_id_length: Optional[int],
                 events_endpoint: Optional[str],
                 identification_endpoint: Optional[str]) -> None:
        self._api_key = api_key
        self._request_timeout = request_timeout
        self._min_id_length = min_id_length
        self._on_error = on_error
        self._queue: queue.Queue = AsyncConsumer.create_queue()
        self._endpoints = {
            "events": Endpoint(url=events_endpoint or "https://api.amplitude.com/2/httpapi", is_json=True),
            "identification": Endpoint(url=identification_endpoint or "https://api.amplitude.com/identify",
                                       is_json=False),
        }
        self._session = Session()
        self._consumer = AsyncConsumer(message_queue=self._queue,
                                       do_upload=self._upload_batch,
                                       flush_queue_size=flush_queue_size,
                                       flush_interval=flush_interval)
        atexit.register(self.shutdown)
        self._consumer.start()

    def track(self, user_id: str, event_name: str, properties: Optional[Dict[str, Any]], metadata: Optional[AmplitudeMetadata]) -> None:
        data = {k: v for (k, v) in vars(metadata).items() if v is not None} if metadata is not None else {}
        data["user_id"] = user_id
        data["event_type"] = event_name
        data["event_properties"] = properties if properties is not None else {}
        if "time" not in data:
            data["time"] = int(time.time() * 1000)
        self._enqueue(AsyncConsumerMessage("events", data))

    def identify(self, user_id: str, properties: Optional[Dict[str, Any]], metadata: Optional[AmplitudeMetadata]) -> None:
        data = {k: v for (k, v) in vars(metadata).items() if v is not None} if metadata is not None else {}
        data["user_id"] = user_id
        data["user_properties"] = properties if properties is not None else {}
        self._enqueue(AsyncConsumerMessage("identification", data))

    def _upload_batch(self, batch: List[AsyncConsumerMessage], stop_event: Event) -> None:
        message_type = batch[0].message_type
        endpoint_url, is_json = self._endpoints[message_type]
        try:
            if message_type == "events":
                data = {
                    "events": [message.data for message in batch],
                    "api_key": self._api_key,
                }
                if self._min_id_length is not None:
                    data["options"] = {"min_id_length": self._min_id_length}
            else:
                data = {
                    "identification": json.dumps([message.data for message in batch]),
                    "api_key": self._api_key
                }
            self._send_request(Request(url=endpoint_url, is_json=is_json, data=data))
        except Exception as e:
            self._on_error(str(e))

    def _send_request(self, request: Request) -> None:
        if request.is_json:
            response = self._session.post(request.url, json=request.data, timeout=self._request_timeout.total_seconds())
        else:
            response = self._session.post(request.url, data=request.data, timeout=self._request_timeout.total_seconds())
        if response.status_code >= 300:
            self._on_error(f'Unexpected status code for {request.url}: {response.status_code}')

    def shutdown(self) -> None:
        self._consumer.shutdown()

    def _enqueue(self, message: AsyncConsumerMessage) -> None:
        try:
            self._queue.put(message)
        except queue.Full:
            self._on_error("async queue is full")

    def flush(self) -> None:
        self._consumer.flush()
コード例 #6
0
def test_consumer():
    batches = []

    q = AsyncConsumer.create_queue()
    consumer = AsyncConsumer(
        message_queue=q,
        do_upload=lambda batch, event: batches.append([msg.data for msg in batch]),
        flush_queue_size=3,
        flush_interval=timedelta(seconds=1)
    )
    try:
        consumer.start()

        q.put(AsyncConsumerMessage(message_type='data', data='1'))
        q.put(AsyncConsumerMessage(message_type='data', data='2'))

        time.sleep(0.1)
        assert batches == []

        q.put(AsyncConsumerMessage(message_type='data', data='3'))
        time.sleep(0.1)

        assert batches == [["1", "2", "3"]]

        q.put(AsyncConsumerMessage(message_type='data', data='4'))

        time.sleep(0.1)
        assert batches == [["1", "2", "3"]]

        consumer.flush()

        time.sleep(0.1)
        assert batches == [["1", "2", "3"], ["4"]]

        consumer.flush()
        consumer.flush()

        q.put(AsyncConsumerMessage(message_type='data', data='5'))
        q.put(AsyncConsumerMessage(message_type='data', data='6'))

        time.sleep(0.1)
        assert batches == [["1", "2", "3"], ["4"]]

        time.sleep(1)
        assert batches == [["1", "2", "3"], ["4"], ["5", "6"]]

        q.put(AsyncConsumerMessage(message_type='message', data='7'))
        q.put(AsyncConsumerMessage(message_type='data', data='8'))

        time.sleep(0.1)
        assert batches == [["1", "2", "3"], ["4"], ["5", "6"], ["7"]]

        consumer.flush()

        time.sleep(0.1)
        assert batches == [["1", "2", "3"], ["4"], ["5", "6"], ["7"], ["8"]]

        q.put(AsyncConsumerMessage(message_type='data', data='9'))
        q.put(AsyncConsumerMessage(message_type='data', data='10'))
    finally:
        consumer.shutdown()

        time.sleep(0.1)
        assert batches == [["1", "2", "3"], ["4"], ["5", "6"], ["7"], ["8"], ["9", "10"]]
コード例 #7
0
class BrazeClient:
    def __init__(
        self,
        base_url: str,
        api_key: str,
        flush_queue_size: int,
        flush_interval: timedelta,
        request_timeout: timedelta,
        logger: Logger,
    ) -> None:
        self._api_key = api_key
        self._request_timeout = request_timeout
        self._queue: queue.Queue = AsyncConsumer.create_queue()
        base_url = base_url.rstrip("/")
        self._user_track_url = f'{base_url}/users/track'
        self._session = Session()
        self._logger = logger
        self._consumer = AsyncConsumer(message_queue=self._queue,
                                       do_upload=self._upload_batch,
                                       flush_queue_size=flush_queue_size,
                                       flush_interval=flush_interval)
        atexit.register(self.shutdown)
        self._consumer.start()

    def identify(self, user_id: str, properties: Optional[Dict[str,
                                                               Any]]) -> None:
        data = self._to_braze_properties(properties)
        data["external_id"] = user_id
        self._enqueue(AsyncConsumerMessage("", {"attributes": data}))

    def track(self, user_id: str, event_name: str,
              properties: Optional[Dict[str, Any]]) -> None:
        data = {
            "external_id": user_id,
            "name": event_name,
            "time": datetime.now().isoformat(),
            "properties": self._to_braze_properties(properties),
        }
        self._enqueue(AsyncConsumerMessage("", {"events": data}))

    def _upload_batch(self, batch: List[AsyncConsumerMessage],
                      stop_event: Event) -> None:
        body = {}
        count = 0
        for event in batch:
            for key, value in event.data.items():
                if key not in body:
                    body[key] = []
                count += 1
                body[key].append(value)

        self._logger.info(f"uploading {count} items")
        try:
            response = self._session.post(
                self._user_track_url,
                headers={'Authorization': f'Bearer {self._api_key}'},
                json=body,
                timeout=self._request_timeout.total_seconds(),
            )
            if response.status_code >= 300:
                self._logger.error(
                    f'unexpected response status: {response.status_code}')
            else:
                self._logger.info(f'response status: {response.status_code}')
        except Exception as e:
            self._logger.error(str(e))

    def flush(self) -> None:
        self._consumer.flush()

    def shutdown(self) -> None:
        self._consumer.shutdown()

    def _enqueue(self, message: AsyncConsumerMessage) -> None:
        try:
            self._queue.put(message)
        except queue.Full:
            self._logger.error("async queue is full")

    @staticmethod
    def _to_braze_properties(
            properties: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        if properties is None:
            return None
        return {
            key: BrazeClient._value_for_api(value)
            for key, value in properties.items()
        }

    @staticmethod
    def _value_for_api(value: Any) -> Any:
        # https://www.braze.com/docs/api/objects_filters/user_attributes_object/
        # https://www.braze.com/docs/api/objects_filters/event_object/
        # API doesn't support objects and (non-string) arrays.
        return json.dumps(value) if isinstance(value, (list, dict)) else value