# Python bulk import animals

**URL:** <https://farmos.discourse.group/t/python-bulk-import-animals/2193>\
**Category:** Hosting farmOS\
**Created:** [January 3, 2025, 10:08pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193 "2025-01-03T22:08:05Z")\
**Posts on this page:** 14\
**Page:** 1

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [January 3, 2025, 10:08pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/1 "2025-01-03T22:08:05Z")

</div>

Morning/ evening/ whatever time is is for you folk.

I am working on a script that can be executed from any machine to import an animal and make sure the breed/species is also set. does anyone have a working script i can look over to see where i am missing things?

Currently using this script.

```auto
from farmOS import farmOS
from datetime import datetime
import csv

hostname = "farming. *****.*******"
username = *****************
password = **********************

# Maintain an external state of the token.
current_token = None

# Callback function to save new tokens.
def token_updater(new_token):
    print(f"Got a new token! {new_token}")
    # Update state.
    current_token = new_token

# Create the client.
farm_client = farmOS(
    hostname=hostname,
    token_updater=token_updater, # Provide the token updater callback.
)

# Authorize the client (initial or refresh token):
if not current_token:
    current_token = farm_client.authorize(username, password, scope="farm_manager")
else:
    # Use existing token if available (optional refresh logic)
    pass

# Create the animal
animal_data = {
    "attributes": {
        "name": "My Great Planting",
    },
}

response = farm_client.asset.send('animal', animal_data)

```

but i am getting an error at the last line saying  
“”“422 Client Error: Unprocessable Content for url”“”

Any assistance would be greatly helpful.

---

<div class="post-metadata">

**Author:** ![Farmer-Ed](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/farmer-ed/32/662_2.png) [@Farmer-Ed](https://farmos.discourse.group/u/Farmer-Ed)\
**Post date:** [January 4, 2025, 5:34am UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/2 "2025-01-04T05:34:09Z")

</div>

I haven’t done this in Python, but have using Node-Red and just using JS.

Is there a message field in the response with a more detailed description of what’s failing, it may give you a better idea of what the issue is.

---

<div class="post-metadata">

**Author:** ![Symbioquine](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/symbioquine/32/284_2.png) [@Symbioquine](https://farmos.discourse.group/u/Symbioquine)\
**Post date:** [January 4, 2025, 12:52pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/3 "2025-01-04T12:52:29Z")

</div>

Did you check out my tutorial blog post on python scripting with farmOS?

> **[Introduction to Python Scripting with farmOS](https://farmos.org/blog/2023/quick-intro-to-python-scripting/)**
>
> farmOS is a free and open source web-based application for farm management, planning, and record keeping.

---

<div class="post-metadata">

**Author:** ![mstenta](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/mstenta/32/4_2.png) [@mstenta](https://farmos.discourse.group/u/mstenta)\
**Post date:** [January 4, 2025, 1:47pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/4 "2025-01-04T13:47:43Z")

</div>

Welcome to the forum @mattman0123!

> [@mattman0123](#):
>
> import an animal and make sure the breed/species is also set

Sounds like you’ve already identified the issue! The “Animal Type” (`animal_type`) field is required on `animal` assets (aka “Species/breed”). It belongs in the `relationships` of the asset. If you create an animal through the farmOS UI you can look at it in the API to see how `animal_type` needs to be formatted. It needs to reference the UUID of a `taxonomy_term` in the `animal_type` vocabulary.

You can fetch a list of `animal_type` terms from the API to find the UUID of the term you want. If the term does not exist, you will need to create it first before you can reference it on the `animal` asset.

Once you have the UUID, I think your code will look something like this:

```python
# Create the animal
animal_data = {
    "attributes": {
        "name": "My Great Animal",
    },
    "relationships": {
        "animal_type": {
            "data": {
                "id": "5d9cce61-032f-4dba-b67c-999429505a12",
            },
        },
    },
}

```

---

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [January 6, 2025, 2:34pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/5 "2025-01-06T14:34:11Z")

</div>

good to know this is a required. I will iterate through the animal\_type and see if i can figure out how to add that into the import.

---

<div class="post-metadata">

**Author:** ![Symbioquine](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/symbioquine/32/284_2.png) [@Symbioquine](https://farmos.discourse.group/u/Symbioquine)\
**Post date:** [January 6, 2025, 2:47pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/6 "2025-01-06T14:47:29Z")

</div>

> [@mattman0123](#):
>
> I will iterate through the animal\_type and see if i can figure out how to add that into the import.

That’s already covered in the tutorial blog post I linked:

 ![image](https://global.discourse-cdn.com/free1/uploads/farm_os/original/2X/f/f0ff71dafdba73a737cdacc778ad952e39c566c6.png)

I expect it shouldn’t take much to adapt it to your needs.

---

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [January 6, 2025, 3:03pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/7 "2025-01-06T15:03:49Z")

</div>

This is perfect looking at this now.  
ill update once i am done.

---

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [January 6, 2025, 3:23pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/8 "2025-01-06T15:23:15Z")

</div>

SOLUTION SO FAR:

```auto
from farmOS import farmOS
from datetime import datetime
import pandas as pd
import csv

hostname = "farming. ****************.cloud"
username = " *************"
password = " *************"

# Maintain an external state of the token.
current_token = None

# Callback function to save new tokens.
def token_updater(new_token):
    print(f"Got a new token! {new_token}")
    # Update state.
    current_token = new_token

# Create the client.
farm_client = farmOS(
    hostname=hostname,
    token_updater=token_updater, # Provide the token updater callback.
)

# Authorize the client (initial or refresh token):
if not current_token:
    current_token = farm_client.authorize(username, password, scope="farm_manager")
else:
    # Use existing token if available (optional refresh logic)
    pass

def create_find_animal_type(data):
    animal_type_search = farm_client.term.iterate(
        'animal_type',
        params=farm_client.filter('name', data),
    )
    animal_type = next(iter(animal_type_search), None)

    # If the animal type does not already exist, create it
    if animal_type is None:
        term_create_response = farm_client.term.send(
            'animal_type',
            {"attributes": {"name": data}}
        )
        animal_type = term_create_response["data"]
    return animal_type

with open(r"csv_asset--animal.csv", newline='') as csvfile:
    csv_reader = csv.DictReader(csvfile)
    for animal in csv_reader:
        animal_dob = datetime.strptime(animal['birthdate'], "%Y-%m-%d")
        animal_type = create_find_animal_type(animal["animal type"])
        # Create the animal
        animal_create_response = farm_client.asset.send('animal', {
            "attributes": {
                "name": animal['name'],
                "sex": animal['sex'],
                "birthdate": animal_dob.strftime('%Y-%m-%dT%H:%M:%S+00:00'),
                "is fixed": animal["is fixed"],
            },
            "relationships": {
                "animal_type": {
                    # Make each animal a animal_type
                    "data": {
                        "type": animal_type['type'],
                        "id": animal_type['id'],
                    },
                },
            },
        })

        print("Created {!r}: {}/asset/{}".format(
            animal['name'],
            farm_client.session.hostname,
            animal_create_response['data']['attributes']['drupal_internal__id'],
        ))

```

this worked great for importing 100 hens into my coop. Then i manually moved them to where they needed to be. Also created the animal\_type as required.

CSV was formatted as follows:

name,is fixed, animal type, birthdate, sex

---

<div class="post-metadata">

**Author:** ![mstenta](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/mstenta/32/4_2.png) [@mstenta](https://farmos.discourse.group/u/mstenta)\
**Post date:** [January 6, 2025, 9:33pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/9 "2025-01-06T21:33:40Z")

</div>

@mattman0123 Cool! Glad you got it working!

> [@mattman0123](#):
>
> CSV was formatted as follows:
> 
> name,is fixed, animal type, birthdate, sex

Curious… is there a reason you’re allowing `is_fixed` to be changed? For `animal` assets that should generally be set to `FALSE` all the time, unless you’re doing something intentionally?

For more info on `is_fixed`, see these data model docs: [Location | farmOS](https://farmos.org/model/logic/location/)

---

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [January 8, 2025, 4:18am UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/10 "2025-01-08T04:18:20Z")

</div>

It was just included in the default CSV I created when I accidentally thought it was for castrated or not.

Next CSV may remove that. I might adjust the script to auto bring in anything from the CSV that is not empty and auto create the payload a little nicer.

---

<div class="post-metadata">

**Author:** ![mstenta](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/mstenta/32/4_2.png) [@mstenta](https://farmos.discourse.group/u/mstenta)\
**Post date:** [January 8, 2025, 1:15pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/11 "2025-01-08T13:15:44Z")

</div>

> [@mattman0123](#):
>
> It was just included in the default CSV I created when I accidentally thought it was for castrated or not.

👍 Yea it’s not the most intuitive name.

On a related note, we will probably be changing `is_castrated` to `is_sterile` in farmOS 4.x, so be aware of that when the time comes to upgrade: [https://www.drupal.org/project/farm/issues/3494784](https://www.drupal.org/project/farm/issues/3494784)

---

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [January 8, 2025, 5:55pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/12 "2025-01-08T17:55:22Z")

</div>

added all the script to github, added more details and validation for the CSV.  
For some reason my instance can not use the CSV import properly. so i built this up. This is also a way for me to automate some of the animal sales we conduct.

> **[GitHub - mattman0123/FarmOS-Animal-Importer: A Python script to bulk import...](https://github.com/mattman0123/FarmOS-Animal-Importer/tree/main)**
>
> main

---

<div class="post-metadata">

**Author:** ![mstenta](https://yyz2.discourse-cdn.com/free1/user_avatar/farmos.discourse.group/mstenta/32/4_2.png) [@mstenta](https://farmos.discourse.group/u/mstenta)\
**Post date:** [January 8, 2025, 6:14pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/13 "2025-01-08T18:14:14Z")

</div>

> [@mattman0123](#):
>
> added all the script to github, added more details and validation for the CSV.

Thanks for sharing @mattman0123! I’m sure others will find this useful in the future.

> [@mattman0123](#):
>
> For some reason my instance can not use the CSV import properly.

I wonder if it’s a CSV upload issue? Are you able to upload other files, like images/files on assets/logs? Happy to help debug this if you want… maybe in a separate topic or chat.

---

<div class="post-metadata">

**Author:** ![mattman0123](https://avatars.discourse-cdn.com/v4/letter/m/b9e5f3/32.png) [@mattman0123](https://farmos.discourse.group/u/mattman0123)\
**Post date:** [May 21, 2025, 5:47pm UTC](https://farmos.discourse.group/t/python-bulk-import-animals/2193/14 "2025-05-21T17:47:23Z")

</div>

I have been alittle busy with the farm now. But i have not needed it again, so i have not checked what else was broken. But i will be rebuilding it on a new Docker Host soon, with a new file structure.
