# python-content-utils

This library is for common logic/functionality in content-review microservices and lambdas. Currently used by:

* [lambda-content](https://github.com/theorchard/lambda-content)

## Development

```sh
# Install local development dependencies
make pip_dev

# Only install the library locally
make local_install

# clean local env
make clean

# test locally
make lint test

# test integration locally
# make sure you copy the env file in tests/integration first
cd tests/integration
cp .env.shadow .env

make test_integration_local
```

## Connectors

### Content Lambda Logger

The content lambda logger class standardizes log output across lambdas.
It can be used by lambdas that process a single or batched input. When used with
the `EventSourceMessage` class, it will handle parsing MSK messages for
data that can be passed to the logs.

The default logging data is:

```jsonc
{
    "status": "error", // either 'error', 'success', or 'skip'
    "result": "",
    "event_key": "", // used to differentiate batched messages
    "product_id": "",
    "message": "" // field which is displayed by default in datadog logs
}
```

The logging level will be derived by the 'status' value.

* _success_: INFO
* _skip_: WARNING
* _error_: ERROR

#### Example

```python
from content_utils.logging import ContentLambdaLoggerCDC
from content_utils.msk.event_source_message import EventSourceMessage
from app import config
content_lambda_logger = ContentLambdaLoggerCDC(config.app_logger)


def handler(event, context):
    """Lambda Entry point."""
    msk_event = EventSourceMessage(event)
    for event_key, replication_event in msk_event:
        content_lambda_logger.start(replication_event, event_key)
        process_event(replication_event)
        content_lambda_logger.end()


def process_event(replication_event):
    content_lambda_logger.set_data(status='skip', result='skipped')

```

### LambdaOpensearchConnector

This allows you to connect to OpenSearch and provides some common functionality currently used by the Lambdas.

#### Usage

By default, the connector will attempt to sign requests using AWS credentials
from the environment.

```python
from content_utils.connectors.opensearch import LambdaOpensearchConnector

os_connector = LambdaOpensearchConnector(OPENSEARCH_ENDPOINT, APP_LOGGER)
```

If you wish to sign requests using an assumed role, pass in the ARN of the
role to assume using the `assume_role_arn` argument:

```python
os_connector = LambdaOpensearchConnector(
    OPENSEARCH_ENDPOINT,
    APP_LOGGER,
    assume_role_arn=ASSUME_ROLE_ARN
)
```

The connector also supports authenticating using a username and password:

```python
os_connector = LambdaOpensearchConnector(
    OPENSEARCH_ENDPOINT,
    APP_LOGGER,
    username=OPENSEARCH_USERNAME,
    password=OPENSEARCH_PASSWORD
)
```

The constructor accepts the following optional keyword arguments:

| Name | Type | Default | Description |
|------|------|---------|-------------|
| username | String | None | Username for basic auth |
| password | String | None | Password for basic auth |
| assume_role_arn | String | None | ARN of the role to assume for signing requests |
| port | Integer | 443 | Port to connect on |
| index_name | String | `review.v01` | Name of the underlying index, used by `drop_index` |
| alias_name | String | `review_write` | Alias used for all read/write operations |

#### Class Methods

##### `index_product`

Adds or updates a product in the index via `alias_name`. Raises `IndexingFailedError` if the document is not created or updated.

##### `patch_product`

Partially updates an existing document via `alias_name`. No-ops if the document is not found or has no changed fields. Raises `IndexingFailedError` on shard failure.

##### `remove_product`

Removes a product from the index via `alias_name` by `product_id`. Logs a warning if the document is not found, does not raise.

##### `check_product_existing_status`

Checks the existing status of a product (by row data) and optionally raises `IneligibleEventError` based on expected status.

###### Examples

```python
example_row_data = {'product_id': 1}

# returns the document (by product_id) if found
os_connector.check_product_existing_status(example_row_data, expected_status=None)

# returns the document (by product_id) if found, raises IneligibleEventError if it isn't found
os_connector.check_product_existing_status(example_row_data, expected_status='found')

# raises IneligibleEventError if document (by product_id) is found
os_connector.check_product_existing_status(example_row_data, expected_status='not found')
```
---

### LambdaGraphQLConnector

This allows you to connect to the graphql gateway and provides some common functionality currently used by the Lambdas.

#### Usage

```python
from content_utils.connectors.graphql import LambdaGraphQLConnector

graphql_connector = LambdaGraphQLConnector(
    GRAPHQL_GATEWAY_URL,
    APPLICATION_NAME,
    APP_LOGGER
)
```

The `LambdaGraphQLConnector` constructor accepts the following optional keyword arguments:

| Name | Type | Description |
|------|------|-------------|
| user | [SystemUser](./content_utils/connectors/system_user.py) | An object representing the identity and profile used in GraphQL calls |
| correlation_id | String | The correlation id used for logging cross service requests |
| raise_on_error | boolean | If true, graphql responses that include an `errors` property will raise a `GraphQLError`. If this is not set, your code should evaluate the response for any `errors` that are present |

#### Class Methods

##### `execute`

Executes a given graphql query with the provided parameters.

##### `get_indexable_product`

Returns a formatted product for indexing in the review_queue index.
Note: requires a formatted object `row_data`, as certain required fields for the formatted document are not found in the product query.

Note: When using the default SystemUser this method will only return POST_SUBMISSION validations, the connector must be instantiated with a SystemUser with a profile type other than ContentProfile to fetch PRE_SUBMISSION
validations.

###### Examples

```python
example_row_data = {
    'product_id': 1,
    'review_queue_id': 2,
    'added_at': '2021-10-18T08:18:25.000000Z'
}
graphql_connector.get_indexable_product(example_row_data)
```

##### `get_complete_product`

Returns a formatted product document with product details, label information, artists, tracks, review history, release schedules, corrections, and localizations.
Note: requires a `product_id` parameter.

###### Examples

```python
product_document = graphql_connector.get_complete_product(product_id=12345)
```
