Python in Appwrite
Tags: python, appwrite, api, backend
Published: 2021-10-30 23:15:01

## Intro 
Appwrite is a Self hosted Back-end as a Service ( BaaS ) that allows developers to make apps easier and faster. It is designed to be run on any operating system or platform , and an Appwrite app can be written in most programming languages / frameworks ( Trust me they have lots of SDKs ). 

It is still in its [pre-release](https://github.com/appwrite/appwrite/releases/tag/0.11.0) stage ( first pre-release on sep of 2019 or 2 years ago ) as of time of writing but it has seen substantial work ( 6.8k commits across 119 branches by 207 contributors ) put into it and a large and active [community](https://discord.gg/GSeTUeA) engagement. 

I will be making an Anime list bot with Appwrite and Telegram to show you the ropes.

## Project set up 

### Step 1 : Installing docker

We will be using docker to install Appwrite, docker is an open source containerization platform , that is it allows developers to package their applications into little virtual spaces , isolated from the rest of the system , in what it calls containers.
install docker from [here] (https://www.docker.com/products/docker-desktop)

### Step 2 : Installing Appwrite on Docker. 

#### Linux / Mac ( Any Unix system ): 
```bash
docker run -it --rm \
    --volume /var/run/docker.sock:/var/run/docker.sock \
    --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
    --entrypoint="install" \
    appwrite/appwrite:0.11.0
```
#### Windows CMD (Command Prompt):
```shell
docker run -it --rm ^
    --volume //var/run/docker.sock:/var/run/docker.sock ^
    --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
    --entrypoint="install" ^
    appwrite/appwrite:0.11.0
``` 
#### Windows PS ( Power Shell ): 
```powershell
docker run -it --rm ,
    --volume /var/run/docker.sock:/var/run/docker.sock ,
    --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
    --entrypoint="install" ,
    appwrite/appwrite:0.11.0
```
[Official installation Docs](https://appwrite.io/docs/installation)

**System requirements**
Appwrite requires at least:
* 1 CPU core and 
* 512MB (2GB Recommended) of RAM. 

_Or If ClamAV is installed :_
* 2GB of RAM is required 
since ClamAV uses at least 1GB on its own.


Let the program do its thing, which will take quite a bit of time if you have a lower than average internet connection .
If you see this message you can proceed to the next step. 
![Success demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/470wc9u9m9i9cy5e6zmm.jpg)

**Errors you may encounter**
```
docker: Error response from daemon: status code not OK but 500:
```
![Error demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u7zrsayjcm4ou7leiixw.jpg)

This means that docker does not have permissions to access the directory you want to install on, and can be easily fixed by doing the following :

* Go to Settings > Resources > File Sharing 
* Then add the directory to the list 

[Source](https://www.reddit.com/r/docker/comments/grjwl9/docker_run_rm_v_getting_error_response_from/)


### Step 3 : Accessing the dashboard :

After installing Appwrite you go to localhost or 127.0.0.1 in your preferred browser to access the Appwrite Dashboard. 

For its first run you have to Create a Root user by : 
* Providing it your preferred name , *
* Adding in an Email address , 
* Creating a strong password and 
* Finally accepting the terms and conditions. 

All pretty standard sign up procedures that should land you here when done correctly.

![dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y4oll9bxhg1hay4wssgp.jpg)

### Step 4 : Creating a Project :

Just click on Create project or the + sign on the dashboard and name it however you desire.

Then grab the Project id and Api Endpoint from the settings tab for later use in the SDK .

![id and api end](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/080dtzu8dibcidjo9fgq.jpg)

After noting them down , go to the API keys tab 
Create a new key with all the Scopes you need.

![ key ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pp4bsmqvh4et9olwbbw1.jpg)

### Step 5 : Server setup ( Python )

Install the python Sdk using pip 
```
pip install appwrite
```
install python telegram bot with 
```
pip install python-telegram-bot
```
install jikanPy with 
```
pip install jikanpy
```
install dotenv with 
```
pip install python-dotenv
```
We first need to [Create a telegram bot](https://core.telegram.org/bots#6-botfather) by using
@BotFather on telegram and obtain a bot token.

Then create a new directory and place some boilerplate code to control the bot 

**main.py**
```diff
+ import dotenv
+ 
+ from os import getenv
+ from telegram.ext import Updater
+ from telegram.ext import CommandHandler
+
+ token = dotenv.get_key('.env','TOKEN') or getenv('TOKEN')
+ 
+ updater = Updater(token=token, use_context=True)
+ dispatcher = updater.dispatcher
+ 
+ def start(update, context):
+     context.bot.send_message(
+         chat_id=update.effective_chat.id, 
+         text="Hi there , im alive")
+ 
+ def main():
+     "Main Execution"
+     start_handler = CommandHandler('start', start)
+     dispatcher.add_handler(start_handler)
+     updater.start_polling()
+    
+ if __name__ == '__main__':
+     main()
+ 
```
then add your token in a .env file as follows 

**.env**
```diff
+ TOKEN=<###########:YOUR_TOKEN_HERE-KSDFJAKSDJFHKSJDHF>
```

Next up lets set up the appwrite boiler plate code as follows 
```diff
 import dotenv
 
 from os import getenv
 from telegram.ext import Updater
 from telegram.ext import CommandHandler
+ from appwrite.client import Client

 token = dotenv.get_key('.env','TOKEN') or getenv('TOKEN')
+ ENDPOINT = dotenv.get_key('.env','ENDPOINT') or getenv('ENDPOINT')
+ PROJECTID = dotenv.get_key('.env','PROJECTID') or getenv('PROJECTID')
+ APIKEY = dotenv.get_key('.env','APIKEY') or getenv('APIKEY')

 updater = Updater(token=token, use_context=True)
 dispatcher = updater.dispatcher

+ client = Client()
+ (client
+   .set_endpoint(F'http://{ENDPOINT}/v1') # Your API Endpoint
+   .set_project(PROJECTID) # Your project ID
+   .set_key(APIKEY) # Your secret API key
+ )


 def start(update, context):
     context.bot.send_message(
         chat_id=update.effective_chat.id, 
         text="Hi there , im alive")
 
 def main():
     "Main Execution"
     start_handler = CommandHandler('start', start)
     dispatcher.add_handler(start_handler)
     updater.start_polling()
    
 if __name__ == '__main__':
     main()
 
```
dont forget to add the respective values to the .env file 
```diff
TOKEN=<###########:YOUR_TOKEN_HERE-KSDFJAKSDJFHKSJDHF>
+ ENDPOINT=localhost
+ PROJECTID=617bf##f####d
+ APIKEY=20e###a379c0f306b9637fff7fae6b0cb73ab98453300613d84fdd74fd6c3c947dda#####c9b05e7####ad3e5bc2adc5c782192d4d586f51e2a668c9de5a44d7fa6f43###bc737216e43bbd##faa949cf######fdf9779eceb1e938e4aaba4073eb8c1c0ce7a2b216a75d4f532545dc7795dd92f42d70c#####79069d783a187
```

Next up add a collection with the following rules (key:datatype)
in the Appwrite dashboard 
*Make sure not to include special chars in your keyname*

* userID : numeric 
* MalList : numeric ( array like )
* state : text ( array like )

then add that to the code as follows :
**.env**
```diff
TOKEN=<###########:YOUR_TOKEN_HERE-KSDFJAKSDJFHKSJDHF>
ENDPOINT=localhost
PROJECTID=617bf##f####d
APIKEY=20e###a379c0f306b9637fff7fae6b0cb73ab98453300613d84fdd74fd6c3c947dda#####c9b05e7####ad3e5bc2adc5c782192d4d586f51e2a668c9de5a44d7fa6f43###bc737216e43bbd##faa949cf######fdf9779eceb1e938e4aaba4073eb8c1c0ce7a2b216a75d4f532545dc7795dd92f42d70c#####79069d783a187
+ COLLECTIONID=617c51c613eab
```
**main.py**
```diff=
TOKEN = dotenv.get_key('.env','TOKEN') or getenv('TOKEN')
ENDPOINT = dotenv.get_key('.env','ENDPOINT') or getenv('ENDPOINT')
PROJECTID = dotenv.get_key('.env','PROJECTID') or getenv('PROJECTID')
APIKEY = dotenv.get_key('.env','APIKEY') or getenv('APIKEY')
+ COLLECTIONID = dotenv.get_key('.env', 'COLLECTIONID') or getenv('COLLECTIONID')

```
### Database CRUD Intro ( Create Read Update Delete )

Appwrite offers the database API for storing persistent data.
I will first explain the basics of creating, reading from, updating and deleting with the api. And then explain how i've used it in an actual [demo app](https://github.com/RobiMez/Better_anime_list).


**Initial setup :**
the following code allows access to the database variable which is an instance of Database class.
```py
from appwrite.client import Client
from appwrite.services.database import Database

client = Client()

(client
  .set_endpoint('https://[HOSTNAME_OR_IP]/v1') # Your API Endpoint
  .set_project('5df5acd0d48c2') # Your project ID
  .set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key
)

database = Database(client)
```
#### **Collections**
Collections are the sql equivalent of tables for appwrite.
The API methods to access them are as follows:

**Create collection** :

*Parameters* : 

name : string (required) 
read : array of strings with read perms (required) 
write : array of strings with write perms (required)
rules : array of [ rule objects ](database.create_collection('[NAME]', [], [], [])) (required)

*Returns* :
[collection object](https://appwrite.io/docs/models/collection)

*Code* :
```py
database.create_collection('[NAME]', [], [], [])
```

**List collection** :
Gets a list of a users collections 

*Parameters* : 

search : string ( term to search for max 256 chars)
limit : integer ( default 25 , max 100 )
offset : integer ( default 0 , used for pagination )
orderType: string (ASC for ascending order and DESC for descending )

*Returns* :
[Collection list object](https://appwrite.io/docs/models/collectionList) 

*Code* :
```py
database.list_collections()
```

**Get collection:**
Gets a collection via its unique id.

*Parameters :*

collectionid : string required 

*Returns* :
[Collection object](https://appwrite.io/docs/models/collection) 

*Code *:
```py
database.get_collection('[COLLECTION_ID]')
```

**Update collection :**
Updates a collection via its unique id.

*Parameters :*

collectionid: string required 
name : string required (collection name)
read : array of strings with read perms (required) 
write : array of strings with write perms (required)
rules : array of [ rule objects ](database.create_collection('[NAME]', [], [], [])) (required)

*Returns :*
[Collection object](https://appwrite.io/docs/models/collection) 

*Code :*
```py
database.update_collection('[COLLECTION_ID]', '[NAME]')
```

**Delete collection :**
Deletes a collection via its unique id.

*Parameters :*
collectionid: string required 

*Returns :*
HTTP code 204 

*Code :*
```py
database.delete_collection('[COLLECTION_ID]')
```
#### Documents
Documents are the sql equivalent of records or entries 
and the api methods to access them are as follows 


**Create Document** :

*Parameters* : 

collectionId : string (required) 
data : json object (required) 
read : array of strings with Read perms 
write : array of strings with write perms
parentDocument : string 
parentProperty : string 
parentPropertyType : string 

*Returns* :
[Document object](https://appwrite.io/docs/models/document)

*Code* :
```py
database.create_document('[COLLECTION_ID]', {})
```

**List Document** :
Gets a list of a users documents 

*Parameters* : 

collectionId : string (required)
filters : array of filter strings 
limit : integer ( default 25 , max 100 )
offset: integer ( default 0 , used for pagination)
orderField : string ( field to sort by )
orderType : string (ASC for ascending and DESC for decending)
orderCast : string (int, string, date, time or datetime)
search : string 

*Returns* :
[Documeents list object](https://appwrite.io/docs/models/documentList) 

*Code* :
```py
database.list_documents('[COLLECTION_ID]')
```

**Get Document:**
Gets a document via its unique id.

*Parameters :*

collectionid : string required 
documentId : string required 

*Returns* :
[Document object](https://appwrite.io/docs/models/document) 

*Code *:
```py
database.get_document('[COLLECTION_ID]', '[DOCUMENT_ID]')
```

**Update Document :**
Updates a Document via its unique id.

*Parameters :*

collectionid: string required 
documentId : string required 
data : Json object required 
read : array of strings with read perms 
write : array of strings with write perms 

*Returns :*
[Document object](https://appwrite.io/docs/models/document) 

*Code :*
```py
database.update_document('[COLLECTION_ID]', '[DOCUMENT_ID]', {})
```

**Delete Document :**
Deletes a Document via its unique id.

*Parameters :*
collectionid: string required 
documentId : string required 

*Returns :*
HTTP code 204 

*Code :*
```py
database.delete_document('[COLLECTION_ID]', '[DOCUMENT_ID]')
```

### Implementation in code 

After you have completed the project initialization steps and gotten yourself familiar with the methods you can fork the [project repo](https://github.com/RobiMez/Better_anime_list) and follow along.

Creating a document :
```py

def set_state(update, context):
    """
        Updates the database state and id,
        generally handles the insert and update side of things 
    """

    user_id = update.effective_user.id
    cqd = update.callback_query.data
    state = str(cqd.split('-')[1])
    mal_id = int(cqd.split('-')[-1])

    exists = database.list_documents(
        COLLECTIONID,
        filters=[f'userID={user_id}'])

    if exists['sum'] == 0:
        print('Data not found , create ')
        # been deleted or never existed
        payload = {
            "userID": user_id,
            "MalList": [mal_id],
            "state": [state]
        }
        database.create_document(COLLECTIONID, payload)
        context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=f'Successfully added {mal_id} to list ')
    else:
        # data exists , update
        print('Data exists , update ')
        doc_id = exists['documents'][0]['$id']
        print(exists)
        if mal_id in exists['documents'][0]['MalList']:
            context.bot.send_message(
                chat_id=update.effective_chat.id,
                text=(
                    f'{mal_id} is already on your list\n'
                    'use /update {id} to update its state\n'
                    ))
        else :
            malist = list(exists['documents'][0]['MalList'])
            new_state = list(exists['documents'][0]['state'])
            new_state.append(str(state))
            malist.append(int(mal_id))
            payload = {
                "userID": user_id,
                "MalList": malist,
                "state": new_state
            }
            updated = database.update_document(
                COLLECTIONID,
                doc_id,
                payload)
            context.bot.send_message(
                chat_id=update.effective_chat.id,
                text=f'Added {mal_id} to list. ')

```

In the set_state function of the app in the repo you will find that we use database.create_document , specifically here 
```py
        payload = {
            "userID": user_id,
            "MalList": [mal_id],
            "state": [state]
        }
        database.create_document(COLLECTIONID, payload)
        context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=f'Successfully added {mal_id} to list ')
```
We pass it a payload of key value pairs that conform to the rules we set during the initialization phase, and have it add our anime shows to the collection we created.

and here 
```py
            payload = {
                "userID": user_id,
                "MalList": malist,
                "state": new_state
            }
            updated = database.update_document(
                COLLECTIONID,
                doc_id,
                payload)
```
we see database.update_document in use to update the document in case it exists , instead of creating a new one. it uses one more parameter called doc_id i.e the document id that is parsed from the existing document 
```py
doc_id = exists['documents'][0]['$id']
```

which was fetched by using database.list_documents and passing in a filter where the userID attribute is equal to the current user of the bot (update.effective_user.id) 
```python=
    exists = database.list_documents(
        COLLECTIONID,
        filters=[f'userID={user_id}'])
```

we can see delete actions used in delete_item
```py
def delete_item(update, context):
    """
        deletes entry 
    """
    user_id = update.effective_user.id
    if not len(context.args) == 1:
        context.bot.send_message(
            chat_id=update.effective_chat.id,
            text='Usage : /delete {id} ')
    else:
        # id of anime in question
        try:
            mal_id = int(context.args[0])
            # if doesnt exist ... ignore
            docs = database.list_documents(
                COLLECTIONID,
                filters=[f'userID={user_id}'])
            print(docs)
            if len(docs['documents']) == 0:
                context.bot.send_message(
                    chat_id=update.effective_chat.id,
                    text="Nothing in your list to delete , start adding more via /anime.",
                    parse_mode=ParseMode.HTML)
            else:
                docs = docs['documents'][0]  # there should only be one doc
                doc_id = docs['$id']
                print(docs)
                mal_ids = docs['MalList']
                statuses = docs['state']
                print(mal_ids)
                print(statuses)
                last_items = len(mal_ids) == 1 and len(statuses) == 1
                correct_item = mal_id == mal_ids[0]
                if last_items and correct_item:
                    # delete the whole document as its the last item
                    database.delete_document(COLLECTIONID, doc_id)
                    context.bot.send_message(
                        chat_id=update.effective_chat.id,
                        text=f"Removed {mal_id} From your list , List is now empty .",
                        parse_mode=ParseMode.HTML)
                elif not correct_item:
                    context.bot.send_message(
                        chat_id=update.effective_chat.id,
                        text=f"{mal_id} isnt in your list ",
                        parse_mode=ParseMode.HTML)
                else:
                    index  = mal_ids.index(mal_id)
                    del mal_ids[0]
                    del statuses[0]
                    
                    payload = {
                        "userID": user_id,
                        "MalList": mal_ids,
                        "state": statuses
                    }
                    database.update_document(
                        COLLECTIONID,
                        doc_id,
                        payload)
                    context.bot.send_message(
                        chat_id=update.effective_chat.id,
                        text=f'Deleted {mal_id} from list ')
                    
        except ValueError:
            context.bot.send_message(
                chat_id=update.effective_chat.id,
                text='Usage : /delete {id} ')


```

Specifically at this part where we delete the document if the item is the last in the list , we pass it the collection id and the doc_id and then send a confirmation message to the user 
```py
if last_items and correct_item:
    # delete the whole document as its the last item
    database.delete_document(COLLECTIONID, doc_id)
    context.bot.send_message(
        chat_id=update.effective_chat.id,
        text=f"Removed {mal_id} From your list , List is now empty .",
        parse_mode=ParseMode.HTML)
```

We use database.list_documents in the list_list function to generate a pretty formatted list like this 
```py
    docs = database.list_documents(
        COLLECTIONID,
        filters=[f'userID={user_id}'])

    if len(docs['documents']) == 0:
        context.bot.send_message(
            chat_id=update.effective_chat.id,
            text="No items in your list , start adding them via /anime.",
            parse_mode=ParseMode.HTML)
    else:
        docs = docs['documents'][0]  # there should only be one doc
        print(docs)
        mal_ids = docs['MalList']
        statuses = docs['state']
        anime_list = '<i><b> Your list : </b></i>\n\n'
        i = 0
        for id in mal_ids:
            anime = jikan.anime(int(id))
            anime_list = anime_list + f'[ <code>{anime["mal_id"]}</code> ] '
            anime_list = anime_list + f'<b>{anime["title"]}</b>\n'
            anime_list = anime_list + f'State : {statuses[i]}\n\n'
            anime_list = anime_list + f'Status : {anime["status"]}\n'
            anime_list = anime_list + f'Episodes : {anime["episodes"]}\n'
            genres = [genre['name'] for genre in anime['genres']]
            genre_string = ""
            for genre in genres:
                genre_string = genre_string + f" {genre} ,"
            anime_list = anime_list + f'Genres : {genre_string[0:-1]}\n'
            anime_list = anime_list + '\n'
            print(anime)
            i += 1
        context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=anime_list,
            parse_mode=ParseMode.HTML)

```

We basically iterate over all documents returned by 
```py
    docs = database.list_documents(
        COLLECTIONID,
        filters=[f'userID={user_id}'])
```
Then for each one we fecth that anime from the my anime list api using jikanPy then we do a bit of templates and string manipulation to get the nice list.
```py
        for id in mal_ids:
            anime = jikan.anime(int(id))
            anime_list = anime_list + f'[ <code>{anime["mal_id"]}</code> ] '
            anime_list = anime_list + f'<b>{anime["title"]}</b>\n'
            anime_list = anime_list + f'State : {statuses[i]}\n\n'
            anime_list = anime_list + f'Status : {anime["status"]}\n'
            anime_list = anime_list + f'Episodes : {anime["episodes"]}\n'
            genres = [genre['name'] for genre in anime['genres']]
            genre_string = ""
            for genre in genres:
                genre_string = genre_string + f" {genre} ,"
            anime_list = anime_list + f'Genres : {genre_string[0:-1]}\n'
            anime_list = anime_list + '\n'
            print(anime)
            i += 1
        context.bot.send_message(
            chat_id=update.effective_chat.id,
            text=anime_list,
            parse_mode=ParseMode.HTML)
```
![nice list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emj9h5lvhhzyqs316ces.jpg)
## Thats all 
![kobayashi](https://thumbs.gfycat.com/BleakGorgeousAmoeba-size_restricted.gif)

Hit me up on [github](github.com/robimez) or telegram @robi_mez if you have any suggestions , comments or questions. 
I would also like feedback on this blog as its my first time writing one <3.