Migration of Categories (Collections) using Shopify API
Tags: python, database, programming
Published: 2021-11-14 09:27:49

In this post, we'll use Shopify Admin API to create collections in our store. Before we go on, you are going to need a development store and create a private app where it provides API credentials to interact with store data.

I highly recommend watching the video below to set up a development store in Shopify:

{% youtube XL-Z53sN3f8 %}

If you already have one, then let's start migrating categories to our Shopify store.

##Building Shopify Client

In this part, we are going to build client to interact with Shopify API by sending requests and receiving responses. Simply, we are creating a separate class where it holds all request-response logic and related attributes.  That'll make our project more clean and maintainable in the future.

Create an empty directory named `client` and inside it add `__init__.py` the file which makes it a python package. Next, create another file named `shopify_client.py` and start by adding a class named `ShopifyClient`:

**client/shopify_client.py**

```python
import json
import requests

class ShopifyClient:

    def __init__(self, api_password: str, shop_url: str, endpoint: str):
        self.api_password = api_password
        self.shop_url = shop_url
        self.endpoint = endpoint
        self.headers = {
            "Accept": 'application/json',
            "Content-Type": 'application/json'
        }
```
The constructor includes the main properties of the client that will be used across functions to interact with Shopify API. For each request, we need the  following properties:

`api_password` - Password that is given in the private app of Shopify Dev Store.

`shop_url` - Shop URL of development store.

`endpoint` - Endpoint that will be used to send requests for.

`headers` -  Header rules to include for each request.

Shopify requires an authorization header, so it knows which particular shop data it should serve. At this point, we'll set only API password as value to header named `X-Shopify-Access-Token`. Let's continue by creating a new function named `api_request` that will handle all types of requests:

**client/shopify_client.py**

```python
 def api_request(self, *args, **kwargs):
        self.headers.update({
          "X-Shopify-Access-Token": self.api_password
        })

        context = {
            "method": kwargs.get("method"),
            "url": f"{self.shop_url.rstrip('/')}{self.endpoint}",
            "timeout": 60,
            "headers": self.headers
        }

        if kwargs.get("method") == "POST":
            context["data"] = json.dumps(kwargs.get("data"))
        
        return requests.request(**context).json()
```
Simply, we're updating the headers to include the authorization header as well. Now, let's see what's inside context:.

`method` - It defines the request method type such as GET, POST or DELETE and takes the value from kwargs.

`url` - The shop_url concatenated with endpoint to produce an absolute URL where the requests will be sent.

`timeout` - Maximum time of waiting for a response.

After that, we're checking if the `method` is `POST` then insert `data` inside `context` as well. Once the `context` is built the `request()` function will send it and return a response as a `json`.

##Adding Data Models

Now, we need a dummy dataset where it'll migrate to Shopify. To achieve that, we can use `factory_boy` package to produce fake data based on the specific model.  Create a directory named data and also include `__init__.py` file inside it. Let's create the base structure of our model by using `dataclass`:

**data/entities.py**

```python
from dataclasses import dataclass

@dataclass
class Category:
    title: str
    body_html: str
    published: bool
    sort_order: str
    image: str
```

Where all these attributes are coming from? The attributes represent required fields of [Shopify Collections API](https://shopify.dev/api/admin-rest/2021-10/resources/collection#resource_object).

![shopify api python](https://www.codepylot.dev/content/images/size/w1000/2021/10/shopifycollectionssketch-01.png)

Currently, we only need the fields above to create a Shopify Collection. The list of belonged products will remain empty for now since we don't have one. Now, it's time to create factories using `factory_boy` to produce some data to migrate it later.

**data/factories.py**

```python
import factory
import factory.fuzzy

from data.entities import Category

class CategoryFactory(factory.Factory):

    class Meta:
        model = Category

    title = factory.Faker('name')
    body_html = factory.Faker('sentence')
    published = factory.fuzzy.FuzzyChoice([True, True, True, False])
    sort_order = "manual"
    image = factory.Faker('image_url')
```
This factory above creates instances of `Category` model with fake data based on the attributes we provide by using `Faker`. In other words, we are mocking the `Category` class to test our functionalities.

##Migration of Categories

So, we finished setting up the client and produce some fake data to migrate it later. Now, it's to start creating the base logic which will put everything together and migrate all data to Shopify. Create another file named `category.py` in the root level of your directory:

**category.py**

```python
import logging

from client.shopify_client import ShopifyClient
from data.factories import CategoryFactory

class CategoryMigration:

    def __init__(self):
        self.log = logging.getLogger(__name__)
        logging.basicConfig(level = logging.INFO)

        self.client = ShopifyClient(
            api_password="YOUR_API_PASSWORD",
            shop_url="YOUR_SHOP_URL",
            endpoint=ShopifyStoreAdmin.COLLECTION_ENDPOINT.value
        )
```

We are going to use `logging` module to print data in the console to see the process in real-time. Also, initializing the Shopify client to make it ready for sending requests.

Next, let's use the factory to produce a list of instances with fake data:

```python
def generate_categories(self):
        category_data = CategoryFactory.create_batch(5)

        collections_shopify = []

        for category in category_data:
            collection_row = {
                "custom_collection" : {
                    "title": category.title,
                    "body_html": category.body_html,
                    "published": category.published,
                    "sort_order": category.sort_order,
                    "image": {"src": category.image }
                }
            }
            collections_shopify.append(collection_row)
        return collections_shopify
```

As you see, we are building the [object structure](https://shopify.dev/api/admin-rest/2021-10/resources/collection#resource_object) as Shopify requires and appending it into the list. The python dictionary will be converted to actual `json` in our client whenever request prepared to be sent.

```python
def migrate(self):
        collections_shopify = self.generate_categories()
        for collect in collections_shopify:
            res = self.client.api_request(
                method="POST",
                data=collect
            )
            self.log.info("Response %s", res)


categories_migration = CategoryMigration()
categories_migration.migrate()
```

Lastly, iterate through generated data and send them to Shopify by using POST the method. The logs will let us know what's going on behind the scenes.

Great! Now you can run the file and watch the migration of data to your development store.

```shell
python category.py
```

##Source code available:

{% github https://github.com/thepylot/shopify-migration %}

##Video Explanation with more details

{% youtube QxeRDBnCmLA %}

##Support 🌏
If you feel like you unlocked new skills, please share them with your friends and subscribe to the youtube channel to not miss any valuable information.