# suprsend-py-sdk
This package can be included in a python3 project to easily integrate
with `SuprSend` platform.

We're working towards creating SDK in other languages as well.

### SuprSend SDKs available in following languages
* python3 >= 3.7 (`suprsend-py-sdk`)
* node (`suprsend-node-sdk`)
* java (`suprsend-java-sdk`)

### Installation
`suprsend-py-sdk` is available on PyPI. You can install using pip.
```bash
pip cache purge && pip install suprsend-py-sdk
```
This SDK depends on a system package called `libmagic`. You can install it as follows:
```bash
# On debian based systems
sudo apt install libmagic

# If you are using macOS
brew install libmagic
```

### Usage
Initialize the SuprSend SDK
```python3
from suprsend import Suprsend
# Initialize SDK
supr_client = Suprsend("workspace_key", "workspace_secret")
```

Following example shows a sample request for triggering a workflow.
It triggers a notification to a user with id: `distinct_id`,
email: `user@example.com` & androidpush(fcm-token): `__android_push_fcm_token__`
using template `purchase-made` and notification_category `system`

```python3
# Prepare Workflow body
workflow_body = {
    "name": "Purchase Workflow",
    "template": "purchase-made",
    "notification_category": "system",
    "delay": "15m",  # Check duration format below
    "users": [
        {
            "distinct_id": "0f988f74-6982-41c5-8752-facb6911fb08",
            "$email": ["user@example.com"],
            "$androidpush": ["__android_push_token__"],
        }
    ],
    # delivery instruction. how should notifications be sent, and whats the success metric
    "delivery": {
        "smart": False,
        "success": "seen"
    },
    # data can be any json / serializable python-dictionary
    "data": {
        "first_name": "User",
        "spend_amount": "$10",
        "nested_key_example": {
            "nested_key1": "some_value_1",
            "nested_key2": {
              "nested_key3": "some_value_3",
            },
        }
    }
}

# Trigger workflow
response = supr_client.trigger_workflow(workflow_body)

```
When you call `supr_client.trigger_workflow`, the SDK internally makes an HTTP call to SuprSend
Platform to register this request, and you'll immediately receive a response indicating
the acceptance status.

Note: The actual processing/execution of workflow happens asynchronously.

```python
# If the call succeeds, response will looks like:
{
    "success": True,
    "status": "success",
    "status_code": 202,
    "message": "Message received",
}

# In case the call fails. You will receive a response with success=False
{
    "success": False,
    "status": "fail",
    "status_code": 400/500,
    "message": "error message",
}
```
#### Duration Format
format for specifying duration: `[xx]d[xx]h[xx]m[xx]s`
Where
* `d` stands for days. value boundary: 0 <= `d`
* `h` stands for hours. value boundary: 0 <= `h` <= 23
* `m` stands for minutes. value boundary: 0 <= `m` <= 59
* `s` stands for seconds. value boundary: 0 <= `s` <= 59

Examples:
* 2 days, 3 hours, 12 mins, 23 seconds -> 2d3h12m23s or 02d03h12m23s
* 48 hours -> 2d
* 30 hours -> 1d6h
* 300 seconds -> 5m
* 320 seconds -> 5m20s
* 60 seconds -> 1m

#### Delivery instruction
All delivery options:
```python
delivery = {
    "smart": True/False,
    "success": "seen/interaction/<some-user-defined-success-event>",
    "time_to_live": "<TTL duration>",
    "mandatory_channels": [] # list of mandatory channels e.g ["email"]
}
```
Where
* `smart` (boolean) - whether to optimize for number of notifications sent?
  - Possible values: `True` / `False`
  - Default value: False
  - If False, then notifications are sent on all channels at once.
  - If True, then notifications are sent one-by-one (on regular interval controlled by `time_to_live`)
    on each channel until given `success`-metric is achieved.

* `success` - what is your measurement of success for this notification?
  - Possible values: `seen` / `interaction` / `<some-user-defined-success-event>`
  - Default value: seen
  - If `seen`: If notification on any of the channels is seen by user, consider it a success.
  - If `interaction`: If notification on any of the channels is clicked/interacted by the user, consider it a success.
  - If `<some-user-defined-success-event>`: If certain event is done by user within the event-window (1 day), consider it a success.
    - currently, event-window is not configurable. default set to `1d` (1 day).
      success-event must happen within this event-window since notification was sent.

* `time_to_live` - What's your buffer-window for sending notification.
  - applicable when `smart`=True, otherwise ignored
  - Default value: `1h` (1 hour)
  - notification on each channel will be sent with time-interval of [`time_to_live / (number_of_channels - 1))`] apart.
  - channels are tried in low-to-high notification-cost order based on `Notification Cost` mentioned in Vendor Config.
    If cost is not mentioned, it is considered zero for order-calculation purpose.
  - Process will continue until all channels are exhausted or `success` metric is achieved, whichever occurs first.

* `mandatory_channels` - Channels on which notification has to be sent immediately (irrespective of notification-cost).
  - applicable when `smart`=True, otherwise ignored
  - Default value: [] (empty list)
  - possible channels: `email, sms, whatsapp, androidpush, iospush` etc.


If delivery instruction is not provided, then default value is
```python3
{
    "smart": False,
    "success": "seen"
}
```

### Add attachments

To add one or more Attachments to a Notification (viz. Email, Whatsapp),
call `supr_client.add_attachment(...)` for each file.
Ensure that file_path is proper, otherwise it will raise FileNotFoundError.
```python
# this snippet can be used to add attachment to workflow_body.
file_path = "/home/user/billing.pdf"
supr_client.add_attachment(workflow_body, file_path)
```

#### Attachment structure
The `add_attachment(...)` call appends below structure to `data->'$attachments'`

```json
{
    "filename": "billing.pdf",
    "contentType": "application/pdf",
    "data": "Q29uZ3JhdHVsYXRpb25zLCB5b3UgY2FuIGJhc2U2NCBkZWNvZGUh",
}
```
Where
* `filename` - name of file.
* `contentType` - MIME-type of file content.
* `data` - base64-encoded content of file.

### Limitation
* a single workflow body size must not exceed 200KB (200 * 1024 bytes). While calculating size, attachments are ignored
* if size exceeds above mentioned limit, SDK raises python's builtin ValueError.

### Batching Workflow Requests [_coming-soon..._]
You can batch multiple workflow requests in one call. Use `batch_instance.append(...)` on batch-instance
to add however-many-records to call in batch.
```python3
batch_ins = supr_client.workflow_batch.new()

workflow_body1 = {...}  # must be a proper workflow request json/dict
workflow_body2 = {...}  # must be a proper workflow request json/dict

# --- use .append on batch instance to add one or more records
batch_ins.append(workflow_body1)
batch_ins.append(workflow_body2)
# OR
batch_ins.append(workflow_body1, workflow_body2)

# -------
response = batch_ins.trigger()

print(response)
```
* There isn't any limit on number-of-records that can be added to batch-instance.
* On calling `batch_ins.trigger()` the SDK internally makes one-or-more Callable-chunks.
* each callable-chunk contains a subset of records, the subset calculation is based on each record's bytes-size
  and max allowed chunk-size and chunk-length etc.
* for each callable-chunk SDK makes an HTTP call to SuprSend To register the request.

### Set channels in User Profile
If you regularly trigger a workflow for users on some pre-decided channels,
then instead of adding user-channel-details in each workflow request, you can set those channel-details in user
profile once, and after that, in workflow trigger request you only need to pass the distinct_id of the user.
All associated channels in User profile will be automatically picked when executing the workflow.

You can set user channel details viz. email, sms, whatsapp, androidpush etc (using `user.append` method) as shown in the example below.

```python3
distinct_id = "__uniq_user_id__"  # Unique id of user in your application
# Instantiate User profile
user = supr_client.user.new(distinct_id=distinct_id)
# append channel properties
user.append({
  "$email": "user@example.com",
  "$sms": "+919999999999",
  "$whatsapp": "+919999999999",
  "$androidpush": "__fcm_androidpush_token__"
})
# save
response = user.save()
print(response)

```

```python
# Response structure
{
    "success": True, # if true, request was accepted.
    "status": "success",
    "status_code": 202, # http status code
    "message": "OK",
}

{
    "success": False, # error will be present in message
    "status": "fail",
    "status_code": 500, # http status code
    "message": "error message",
}

```

If you want to remove some channel details, use `user.remove` method.

```python
distinct_id = "__uniq_user_id__"  # Unique id of user in your application
# Instantiate User profile
user = supr_client.user.new(distinct_id=distinct_id)

# remove whatsapp channel
user.remove({
  "$whatsapp": "+919999999999",
})
# save
response = user.save()
print(response)

```
There are helper methods available to add/remove channel details.
```python
# Add channel helper methods
user.add_email("user@example.com")
user.add_sms("+919999999999")
user.add_whatsapp("+919999999999")

user.add_androidpush("__android_push_fcm_token__") # by default, token is assumed to be fcm-token
# You can set the optional provider value [fcm/xiaomi/oppo] if its not a fcm-token
user.add_androidpush("__android_push_xiaomi_token__", provider="xiaomi")

user.add_iospush("__iospush_token__")

# save
response = user.save()
print(response)


# Remove channel helper methods
user.remove_email("user@example.com")
user.remove_sms("+919999999999")
user.remove_whatsapp("+919999999999")
user.remove_androidpush("__android_push_fcm_token__")
user.remove_androidpush("__android_push_xiaomi_token__", provider="xiaomi")
user.remove_iospush("__iospush_token__")
# save
response = user.save()
print(response)
```
Note: After calling `append`/`remove`/`add_*`/`remove_*` methods, don't forget to call `user.save()`.

Once channels details are set at User profile, you only have to mention the user's distinct_id
while triggering workflow. Associated channels will automatically be picked up from user-profile e.g.

```python3
# Prepare Workflow body
workflow_body = {
    "name": "Purchase Workflow",
    "template": "purchase-made",
    "notification_category": "system",
    "delay": "15m",
    "users": [
        {
            "distinct_id": "0f988f74-6982-41c5-8752-facb6911fb08",
        }
    ],
    # data can be any json / serializable python-dictionary
    "data": {
        "first_name": "User",
        "spend_amount": "$10",
        "nested_key_example": {
            "nested_key1": "some_value_1",
            "nested_key2": {
              "nested_key3": "some_value_3",
            },
        }
    }
}

# Trigger workflow
response = supr_client.trigger_workflow(workflow_body)

```

### Track and Send Event
You can track and send events to SuprSend platform by using `supr_client.track` method.
Event: `event_name`, tracked wrt a user: `distinct_id`, with event-attributes: `properties`

```python3
# Method signature:
def track(distinct_id: str, event_name: str, properties: Dict = None) -> Dict
```

```python3
# Example
distinct_id = "__uniq_user_id__" # Mandatory, Unique id of user in your application
event_name = "__event_name__"   # Mandatory, name of the event you're tracking
properties = {...} # Optional, default=None, a dict representing event-attributes

response = supr_client.track(distinct_id, event_name, properties=properties)
print(response)

```

```python
# Response structure
{
    "success": True, # if true, request was accepted.
    "status": "success",
    "status_code": 202, # http status code
    "message": "OK",
}

{
    "success": False, # error will be present in message
    "status": "fail",
    "status_code": 500, # http status code
    "message": "error message",
}

```
