# Firebase-aio

A simple async python wrapper for the [Firebase API](https://firebase.google.com).

## Installation

```python
pip install git+https://github.com/09ghostrider/firebase-aio
```

## Getting Started

### Python Version

Firebase-aio was written for python 3 and will not work correctly with python 2.

### Add firebase-aio to your application

For use with only user based authentication we can create the following configuration:

```python
import firebaseaio
config = {
  "apiKey": "apiKey",
  "authDomain": "projectId.firebaseapp.com",
  "databaseURL": "https://databaseName.firebaseio.com",
  "storageBucket": "projectId.appspot.com"
}
firebase = firebaseaio.initialize_app(config)
```

We can optionally add a [service account credential](https://firebase.google.com/docs/server/setup#prerequisites) to our
configuration that will allow our server to authenticate with Firebase as an admin and disregard any security rules.

```python
import firebaseaio
config = {
  "apiKey": "apiKey",
  "authDomain": "projectId.firebaseapp.com",
  "databaseURL": "https://databaseName.firebaseio.com",
  "storageBucket": "projectId.appspot.com",
  "serviceAccount": "path/to/serviceAccountCredentials.json"
}
firebase = firebaseaio.initialize_app(config)
```

Adding a service account will authenticate as an admin by default for all database queries, check out the
[Authentication documentation](#authentication) for how to authenticate users.

### Use Services

A firebase-aio app can only use Firebase database services.

```firebase.database()``` - 
Check out the [Database documentation](#database) for further details.

## Database

You can build paths to your data by using the ```child()``` method.

```python
db = firebase.database()
db.child("users").child("Morty")
```

### Save Data

#### push

To save data with a unique, auto-generated, timestamp-based key, use the ```push()``` method.

```python
data = {"name": "Mortimer 'Morty' Smith"}
await db.child("users").push(data)
```

#### set

To create your own keys use the ```set()``` method. The key in the example below is "Morty".

```python
data = {"name": "Mortimer 'Morty' Smith"}
await db.child("users").child("Morty").set(data)
```

#### update

To update data for an existing entry use the ```update()``` method.

```python
await db.child("users").child("Morty").update({"name": "Mortiest Morty"})
```

#### remove

To delete data for an existing entry use the ```remove()``` method.

```python
await db.child("users").child("Morty").remove()
```

#### multi-location updates

You can also perform [multi-location updates](https://www.firebase.com/blog/2015-09-24-atomic-writes-and-more.html) with the ```update()``` method.

```python
data = {
    "users/Morty/": {
        "name": "Mortimer 'Morty' Smith"
    },
    "users/Rick/": {
        "name": "Rick Sanchez"
    }
}
await db.update(data)
```

To perform multi-location writes to new locations we can use the ```generate_key()``` method.

```python
data = {
    "users/"+ref.generate_key(): {
        "name": "Mortimer 'Morty' Smith"
    },
    "users/"+ref.generate_key(): {
        "name": "Rick Sanchez"
    }
}
await db.update(data)
```

### Retrieve Data

#### val
Queries return a PyreResponse object. Calling ```val()``` on these objects returns the query data.

```python
users = await db.child("users").get()
print(users.val()) # {"Morty": {"name": "Mortimer 'Morty' Smith"}, "Rick": {"name": "Rick Sanchez"}}
```

#### key
Calling ```key()``` returns the key for the query data.

```python
user = await db.child("users").get()
print(user.key()) # users
```

#### each
Returns a list of objects on each of which you can call ```val()``` and ```key()```.

```python
all_users = await db.child("users").get()
for user in all_users.each():
    print(user.key()) # Morty
    print(user.val()) # {name": "Mortimer 'Morty' Smith"}
```

#### get

To return data from a path simply call the ```get()``` method.

```python
all_users = await db.child("users").get()
```

#### Conditional Requests

It's possible to do conditional sets and removes by using the `conditional_set()` and `conitional_remove()` methods respectively. You can read more about conditional requests in Firebase [here](https://firebase.google.com/docs/reference/rest/database/#section-conditional-requests).

To use these methods, you first get the ETag of a particular path by using the `get_etag()` method. You can then use that tag in your conditional request.

```python
etag = await db.child("users").child("Morty").get_etag()
data = {"name": "Mortimer 'Morty' Smith"}
await db.child("users").child("Morty").conditional_set(data, etag)
```

If the passed ETag does not match the ETag of the path in the database, the data will not be written, and both conditional request methods will return a single key-value pair with the new ETag to use of the following form:

```json
{ "ETag": "8KnE63B6HiKp67Wf3HQrXanujSM=" }
```

Here's an example of checking whether or not a conditional removal was successful:

```python
etag = await db.child("users").child("Morty").get_etag()
response = await db.child("users").child("Morty").conditional_remove(etag)
if "ETag" in response:
    etag = response["ETag"] # our ETag was out-of-date
else:
    print("We removed the data successfully!")
```


#### shallow

To return just the keys at a particular path use the ```shallow()``` method.

```python
all_user_ids = await db.child("users").shallow().get()
```

Note: ```shallow()``` can not be used in conjunction with any complex queries.

#### streaming

You can listen to live changes to your data with the ```stream()``` method.

```python
def stream_handler(message):
    print(message["event"]) # put
    print(message["path"]) # /-K7yGTTEp7O549EzTYtI
    print(message["data"]) # {'title': 'firebaseaio', "body": "etc..."}
my_stream = db.child("posts").stream(stream_handler)
```

You should at least handle `put` and `patch` events. Refer to ["Streaming from the REST API"][streaming] for details.

[streaming]: https://firebase.google.com/docs/reference/rest/database/#section-streaming

You can also add a ```stream_id``` to help you identify a stream if you have multiple running:

```python
my_stream = db.child("posts").stream(stream_handler, stream_id="new_posts")
```

#### close the stream

```python
my_stream.close()
```

### Complex Queries

Queries can be built by chaining multiple query parameters together.

```python
users_by_name = await db.child("users").order_by_child("name").limit_to_first(3).get()
```
This query will return the first three users ordered by name.

#### order_by_child

We begin any complex query with ```order_by_child()```.

```python
users_by_name = await db.child("users").order_by_child("name").get()
```
This query will return users ordered by name.

#### equal_to

Return data with a specific value.

```python
users_by_score = await db.child("users").order_by_child("score").equal_to(10).get()
```
This query will return users with a score of 10.

#### start_at and end_at

Specify a range in your data.

```python
users_by_score = await db.child("users").order_by_child("score").start_at(3).end_at(10).get()
```
This query returns users ordered by score and with a score between 3 and 10.

#### limit_to_first and limit_to_last

Limits data returned.

```python
users_by_score = await db.child("users").order_by_child("score").limit_to_first(5).get()
```
This query returns the first five users ordered by score.

#### order_by_key

When using ```order_by_key()``` to sort your data, data is returned in ascending order by key.

```python
users_by_key = await db.child("users").order_by_key().get()
```

#### order_by_value

When using ```order_by_value()```, children are ordered by their value.

```python
users_by_value = await db.child("users").order_by_value().get()
```

### Helper Methods

#### generate_key

```db.generate_key()``` is an implementation of Firebase's [key generation algorithm](https://www.firebase.com/blog/2015-02-11-firebase-unique-identifiers.html).
See multi-location updates for a potential use case.
#### sort
Sometimes we might want to sort our data multiple times. For example, we might want to retrieve all articles written between a
certain date then sort those articles based on the number of likes.
Currently the REST API only allows us to sort our data once, so the ```sort()``` method bridges this gap.
```python
articles = await db.child("articles").order_by_child("date").start_at(startDate).end_at(endDate).get()
articles_by_likes = db.sort(articles, "likes")
```

### Common Errors

#### Index not defined

Indexing is [not enabled](https://firebase.google.com/docs/database/security/indexing-data) for the database reference.