├── .coveragerc ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pyfirebase ├── __init__.py ├── decorators.py └── firebase.py ├── requirements.txt ├── samples ├── sample.py └── streaming.py ├── setup.py └── tests ├── __init__.py ├── test_firebase.py └── test_utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: G5NaXJW5wi7saJafiDLfaOqcH2nswJ15T 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | install: 5 | - pip install -r requirements.txt 6 | - pip install coveralls 7 | script: 8 | py.test --cov=pyfirebase tests/ 9 | after_success: 10 | coveralls 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chidiebere Nnadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyFirebase 2 | [![Build Status](https://travis-ci.org/andela-cnnadi/pyfirebase.svg?branch=master)](https://travis-ci.org/andela-cnnadi/pyfirebase) 3 | [![Coverage Status](https://coveralls.io/repos/github/andela-cnnadi/pyfirebase/badge.svg?branch=master)](https://coveralls.io/github/andela-cnnadi/pyfirebase?branch=master) 4 | [![PyPI](https://img.shields.io/pypi/v/pyfirebase.svg?maxAge=2592000)]() 5 | [![PyPI](https://img.shields.io/pypi/l/pyfirebase.svg?maxAge=2592000)]() 6 | [![PyPI](https://img.shields.io/pypi/dm/pyfirebase.svg?maxAge=2592000)]() 7 | 8 | 9 | Easy to use Firebase Python Plugin. Built as a wrapper around the Firebase REST HTTP API. Syntax is reminiscent of the simple syntax available in Javascript. Inspired by the [python-firebase](https://github.com/ozgur/python-firebase) library by [ozgur](https://github.com/ozgur). 10 | 11 | ## Installation 12 | 13 | Get started using the PyFirebase library by installing it: 14 | 15 | ``` 16 | pip install pyfirebase 17 | ``` 18 | 19 | ## Usage 20 | 21 | Using the PyFirebase library is extremely easy. This is a sample books library built with it. 22 | 23 | ```py 24 | from pyfirebase import Firebase 25 | 26 | firebase = Firebase(YOUR_FIREBASE_URL) 27 | 28 | # Create a Firebase reference 29 | ref = firebase.ref('books') 30 | 31 | # Get the contents of the reference 32 | books = ref.get() 33 | 34 | # Payload data can be declared using native python data types 35 | payload = {'name': 'Harry Potter and the Prisoner of Azkaban', 'pages': 780} 36 | 37 | # The push operation pushes a new node under this node 38 | book = ref.push(payload) 39 | 40 | # We can get the new node id using the name key 41 | id = book['name'] 42 | 43 | # We can navigate to child nodes 44 | bookref = ref.child(id) 45 | 46 | # We can update specific records in this ref 47 | bookref.update({'pages': 600}) 48 | 49 | # We can also just outright replace the content 50 | bookref.set({'name': 'Harry Potter and the Order of the Phoenix', 'pages': 980}) 51 | 52 | # We can obviously simulate an update with a set 53 | bookref.child('pages').set(790) 54 | 55 | # We can delete the book once we're done 56 | bookref.delete() 57 | 58 | # Create a reference to the root by not passing in paramters to the ref function 59 | root = firebase.ref() 60 | 61 | # This function will be called everytime an event happens 62 | def print_data(event, data): 63 | print event 64 | print data 65 | 66 | # We can the event listener to the root ref. We also specify that print_data should be called on event 67 | root.on('child_changed', callback=print_data) 68 | 69 | 70 | # Extra logic to turn on listener when we quit 71 | def signal_handler(signal, frame): 72 | print "Trying to exit" 73 | root.off() 74 | sys.exit(0) 75 | 76 | # Binding Ctrl + C signal to signal_handler 77 | signal.signal(signal.SIGINT, signal_handler) 78 | signal.pause() 79 | ``` 80 | 81 | ### `Firebase` Methods 82 | 83 | #### `__init__(url)` 84 | 85 | Creating a new `Firebase` object is simply done using the `Firebase` constructor. 86 | 87 | ```py 88 | firebase = Firebase(FIREBASE_URL) 89 | ``` 90 | 91 | #### `ref(reference)` 92 | 93 | Calling the `ref()` method on the firebase object creates and returns a new `FirebaseReference` object. We can use this object to manipulate that reference. 94 | 95 | ```py 96 | ref = firebase.ref(ref_name) 97 | ``` 98 | 99 | ### `FirebaseReference` Methods 100 | 101 | #### `get()` 102 | 103 | This method returns all the content in that ref. An error is thrown if there are issues with permissions or any other HTTP error. 104 | 105 | ```py 106 | results = ref.get() 107 | ``` 108 | 109 | #### `push(payload)` 110 | 111 | This method creates new data under this ref. A random id is generated as the key and the `payload` is set as the value. The id is stored in the `name` key of the returned dictionary. 112 | 113 | ```py 114 | record = ref.push(payload) 115 | id = record['name'] 116 | ``` 117 | 118 | #### `set(payload)` 119 | 120 | This method sets the value of the ref to `payload`. If there was data in this ref before, it replaces it with `payload`. If this ref does not exists, it is created and then stored against the payload. 121 | 122 | ```py 123 | ref.set(payload) 124 | ``` 125 | 126 | #### `update(payload)` 127 | 128 | This method updates the keys in the ref with data specified in the payload. If we had payload with data `{'name': 'Egor'}`, it will replace the `name` key in the ref with the value `Egor`. This is the same things as doing `ref.child(name).set('Egor')`. 129 | 130 | ```py 131 | ref.set(payload) 132 | ``` 133 | 134 | #### `delete()` 135 | 136 | This method deletes the ref. 137 | 138 | ```py 139 | ref.delete() 140 | ``` 141 | 142 | ### Streaming Methods 143 | 144 | #### `on(event_name, callback=callback_func)` 145 | 146 | Called on `FirebaseReference` objects. This method adds a new event listener to the ref. The `callback_func` is called every time the event specified by `event_name` happens. 147 | 148 | Currently supported events are `child_changed` and `child_deleted`. 149 | 150 | ```py 151 | # Simple function to print event and data passed in by the on function 152 | print_data(event, data): 153 | print event 154 | print data 155 | 156 | ref.on('child_changed', callback=print_data) 157 | ``` 158 | 159 | #### `off()` 160 | 161 | This method stops listening on all events for this ref. 162 | 163 | ```py 164 | ref.off() 165 | ``` 166 | 167 | ## TODO 168 | 169 | Some of the pending functionality that needs to be implemented include: 170 | 171 | - Authentication 172 | - Support more events on Streaming API 173 | - Result Filtering 174 | - Priority 175 | - Server Values 176 | -------------------------------------------------------------------------------- /pyfirebase/__init__.py: -------------------------------------------------------------------------------- 1 | from firebase import * 2 | -------------------------------------------------------------------------------- /pyfirebase/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | def validate_payload(func): 5 | @wraps(func) 6 | def func_wrapper(instance, payload): 7 | native_types = [bool, int, float, str, list, tuple, dict] 8 | if type(payload) not in native_types: 9 | raise ValueError("Invalid payload specified") 10 | return func(instance, payload) 11 | return func_wrapper 12 | 13 | 14 | def parse_results(func): 15 | @wraps(func) 16 | def func_wrapper(instance, *args, **kwargs): 17 | results = func(instance, *args, **kwargs) 18 | if results.status_code == 200: 19 | return results.json() 20 | else: 21 | results.raise_for_status() 22 | return func_wrapper 23 | -------------------------------------------------------------------------------- /pyfirebase/firebase.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import socket 4 | import requests 5 | import threading 6 | from decorators import validate_payload, parse_results 7 | from sseclient import SSEClient 8 | 9 | 10 | class FirebaseEvents(object): 11 | CHILD_CHANGED = 0 12 | CHILD_ADDED = 2 13 | CHILD_DELETED = 1 14 | 15 | @staticmethod 16 | def id(event_name): 17 | ev = None 18 | mapping = { 19 | 'child_changed': FirebaseEvents.CHILD_CHANGED, 20 | 'child_added': FirebaseEvents.CHILD_ADDED, 21 | 'child_deleted': FirebaseEvents.CHILD_DELETED 22 | } 23 | try: 24 | ev = mapping.get(event_name) 25 | finally: 26 | return ev 27 | 28 | 29 | class ClosableSSEClient(SSEClient): 30 | def __init__(self, *args, **kwargs): 31 | self.should_connect = True 32 | super(ClosableSSEClient, self).__init__(*args, **kwargs) 33 | 34 | def _connect(self): 35 | if self.should_connect: 36 | super(ClosableSSEClient, self)._connect() 37 | else: 38 | raise StopIteration() 39 | 40 | def close(self): 41 | self.should_connect = False 42 | self.retry = 0 43 | self.resp.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR) 44 | self.resp.raw._fp.fp._sock.close() 45 | 46 | 47 | class EventSourceClient(threading.Thread): 48 | def __init__(self, url, event_name, callback): 49 | self.url = url 50 | self.event_name = event_name 51 | self.callback = callback 52 | super(EventSourceClient, self).__init__() 53 | 54 | def run(self): 55 | try: 56 | self.sse = ClosableSSEClient(self.url) 57 | for msg in self.sse: 58 | event = msg.event 59 | if event is not None and event in ('put', 'patch'): 60 | response = json.loads(msg.data) 61 | if response is not None: 62 | # Default to CHILD_CHANGED event 63 | occurred_event = FirebaseEvents.CHILD_CHANGED 64 | if response['data'] is None: 65 | occurred_event = FirebaseEvents.CHILD_DELETED 66 | 67 | # Get the event I'm trying to listen to 68 | ev = FirebaseEvents.id(self.event_name) 69 | if occurred_event == ev or ev == FirebaseEvents.CHILD_CHANGED: 70 | self.callback(event, response) 71 | except socket.error: 72 | pass 73 | 74 | 75 | class FirebaseReference(object): 76 | def __new__(cls, *args): 77 | if len(args) == 2: 78 | connector = args[0] 79 | if isinstance(connector, Firebase): 80 | if args[1] is None or FirebaseReference.is_valid(args[1]): 81 | return super(FirebaseReference, cls).__new__(cls) 82 | return None 83 | 84 | def __init__(self, connector, reference=None): 85 | self.connector = connector 86 | self.current = reference or '' 87 | 88 | def child(self, reference): 89 | if not FirebaseReference.is_valid(reference): 90 | raise ValueError("Invalid reference value") 91 | self.current = "{}/{}".format(self.current, reference) 92 | return self 93 | 94 | @parse_results 95 | @validate_payload 96 | def push(self, payload): 97 | return requests.post(self.current_url, json=payload) 98 | 99 | @parse_results 100 | def get(self): 101 | return requests.get(self.current_url) 102 | 103 | @parse_results 104 | @validate_payload 105 | def set(self, payload): 106 | return requests.put(self.current_url, json=payload) 107 | 108 | @parse_results 109 | @validate_payload 110 | def update(self, payload): 111 | return requests.patch(self.current_url, json=payload) 112 | 113 | @parse_results 114 | def delete(self): 115 | return requests.delete(self.current_url) 116 | 117 | @staticmethod 118 | def is_valid(reference): 119 | pattern = re.compile('^[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)*$') 120 | matches = pattern.match(reference) 121 | if matches: 122 | return True 123 | return False 124 | 125 | @property 126 | def current_url(self): 127 | base = self.connector.FIREBASE_URL 128 | return "{}/{}.json".format(base, self.current) 129 | 130 | def patch_url(self): 131 | if self.current == '': 132 | return self.current_url 133 | base = self.connector.FIREBASE_URL 134 | return "{}/{}/.json".format(base, self.current) 135 | 136 | def on(self, event_name, **kwargs): 137 | url = self.patch_url() 138 | callback = kwargs.get('callback', None) 139 | if event_name is None or callback is None: 140 | raise AttributeError( 141 | 'No callback parameter provided' 142 | ) 143 | if FirebaseEvents.id(event_name) is None: 144 | raise AttributeError( 145 | 'Unsupported event' 146 | ) 147 | # Start Event Source Listener on this ref on a new thread 148 | self.client = EventSourceClient(url, event_name, callback) 149 | self.client.start() 150 | return True 151 | 152 | def off(self): 153 | try: 154 | # Close Event Source Listener 155 | self.client.sse.close() 156 | self.client.join() 157 | return True 158 | except Exception: 159 | print "Error while trying to end the thread. Try again!" 160 | 161 | 162 | class Firebase(object): 163 | FIREBASE_URL = None 164 | 165 | def __new__(cls, *args): 166 | if len(args) == 1: 167 | if Firebase.is_valid_firebase_url(args[0]): 168 | return super(Firebase, cls).__new__(cls) 169 | return None 170 | 171 | def __init__(self, url): 172 | self.FIREBASE_URL = url.strip('/') 173 | 174 | @staticmethod 175 | def is_valid_firebase_url(url): 176 | pattern = re.compile( 177 | r'^https://[a-zA-Z0-9_\-]+\.firebaseio(-demo)?\.com/?$' 178 | ) 179 | matches = pattern.match(url) 180 | if matches: 181 | return True 182 | return False 183 | 184 | def ref(self, reference=None): 185 | ref = FirebaseReference(self, reference) 186 | if ref is None: 187 | raise Exception( 188 | "Something went wrong when trying to create your ref" 189 | ) 190 | return ref 191 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.1 2 | funcsigs==1.0.2 3 | mock==2.0.0 4 | pbr==1.10.0 5 | py==1.4.31 6 | pytest==2.9.1 7 | pytest-cov==2.2.1 8 | requests==2.10.0 9 | six==1.10.0 10 | sseclient==0.0.12 11 | wheel==0.24.0 12 | -------------------------------------------------------------------------------- /samples/sample.py: -------------------------------------------------------------------------------- 1 | from pyfirebase import Firebase 2 | 3 | firebase = Firebase('https://apitest-72809.firebaseio.com/') 4 | 5 | # Create a Firebase reference 6 | ref = firebase.ref('books') 7 | 8 | # Get the contents of the reference 9 | books = ref.get() 10 | 11 | # Payload data can be declared using native python data types 12 | payload = {'name': 'Harry Potter and the Prisoner of Azkaban', 'pages': 780} 13 | 14 | # The push operation pushes a new node under this node 15 | book = ref.push(payload) 16 | 17 | # We can get the new node id using the name key 18 | id = book['name'] 19 | 20 | # We can navigate to child nodes 21 | bookref = ref.child(id) 22 | 23 | # We can update specific records in this ref 24 | bookref.update({'pages': 600}) 25 | 26 | # We can also just outright replace the content 27 | bookref.set({'name': 'Harry Potter and the Order of the Phoenix', 'pages': 980}) 28 | 29 | # We can obviously simulate an update with a set 30 | bookref.child('pages').set(790) 31 | 32 | # We can delete the book once we're done 33 | bookref.delete() 34 | -------------------------------------------------------------------------------- /samples/streaming.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import signal 3 | from pyfirebase import Firebase 4 | 5 | firebase = Firebase('https://apitest-72809.firebaseio.com/') 6 | 7 | # Create a reference to the root by not passing in paramters to the ref function 8 | root = firebase.ref() 9 | 10 | 11 | # This function will be called everytime the event happens 12 | def print_data(event, data): 13 | print event 14 | print data 15 | 16 | # We can the event listener to the root ref. We also specify that print_data should be called on event 17 | root.on('child_changed', callback=print_data) 18 | 19 | 20 | # Extra logic to turn on listener when we quit 21 | def signal_handler(signal, frame): 22 | print "Trying to exit" 23 | root.off() 24 | sys.exit(0) 25 | 26 | # Binding Ctrl + C signal to signal_handler 27 | signal.signal(signal.SIGINT, signal_handler) 28 | signal.pause() 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | 10 | setup(name='pyfirebase', 11 | version='1.3', 12 | description="Firebase Python Plugin", 13 | long_description="Built as an improvement on the python-firebase package built by ozgur", 14 | classifiers=[ 15 | 'Environment :: Console', 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python :: 2.7', 20 | 'Natural Language :: English', 21 | ], 22 | keywords='firebase python pyfirebase', 23 | author='Chidiebere Nnadi', 24 | author_email='chidiebere.nnadi@gmail.com', 25 | maintainer='Chidiebere Nnadi', 26 | maintainer_email='chidiebere.nnadi@gmail.com', 27 | url='http://github.com/andela-cnnadi/pyfirebase', 28 | license='MIT', 29 | packages=['pyfirebase'], 30 | test_suite='tests', 31 | tests_require=['pytest'], 32 | install_requires=['requests>=2.10.0', 'mock', 'sseclient'], 33 | zip_safe=False, 34 | ) 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afropolymath/pyfirebase/11868007a7ff7fec45ed87cec18466e351cdb5ab/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_firebase.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from mock import MagicMock 3 | from unittest import TestCase 4 | from pyfirebase import Firebase, FirebaseReference 5 | 6 | TEST_FIREBASE_URL = 'https://samplechat.firebaseio-demo.com' 7 | 8 | 9 | class TestFirebase(TestCase): 10 | def setUp(self): 11 | self.firebase = Firebase(TEST_FIREBASE_URL) 12 | self.assertIsInstance(self.firebase, Firebase) 13 | 14 | def test_ref(self): 15 | node_ref = self.firebase.ref('test_ref') 16 | self.assertIsInstance(node_ref, FirebaseReference) 17 | self.assertEqual(node_ref.current, 'test_ref') 18 | 19 | def test_child(self): 20 | node_ref = self.firebase.ref('test_ref') 21 | self.assertEqual(node_ref.current, 'test_ref') 22 | child_ref = node_ref.child('test_child_ref') 23 | self.assertEqual(child_ref.current, 'test_ref/test_child_ref') 24 | 25 | def test_current_url_property(self): 26 | node_ref = self.firebase.ref('test_ref') 27 | self.assertEqual(node_ref.current_url, "{}/{}.json".format( 28 | TEST_FIREBASE_URL, 29 | 'test_ref') 30 | ) 31 | 32 | @mock.patch('requests.get') 33 | def test_get_event(self, mock_requests_get): 34 | node_ref = self.firebase.ref('test_ref') 35 | results = node_ref.get() 36 | self.assertTrue(mock_requests_get.called) 37 | mock_requests_get.assert_called_with(node_ref.current_url) 38 | 39 | @mock.patch('requests.put') 40 | def test_set_event(self, mock_requests_put): 41 | node_ref = self.firebase.ref('test_ref') 42 | result = node_ref.set('val') 43 | self.assertTrue(mock_requests_put.called) 44 | mock_requests_put.assert_called_with(node_ref.current_url, json='val') 45 | 46 | @mock.patch('requests.post') 47 | def test_push_event(self, mock_requests_post): 48 | node_ref = self.firebase.ref('test_ref') 49 | result = node_ref.push('val') 50 | self.assertTrue(mock_requests_post.called) 51 | mock_requests_post.assert_called_with(node_ref.current_url, json='val') 52 | 53 | @mock.patch('requests.delete') 54 | def test_delete_event(self, mock_requests_delete): 55 | node_ref = self.firebase.ref('test_ref') 56 | result = node_ref.delete() 57 | self.assertTrue(mock_requests_delete.called) 58 | mock_requests_delete.assert_called_with(node_ref.current_url) 59 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pyfirebase import Firebase, FirebaseReference 3 | 4 | class TestUtils(TestCase): 5 | def test_valid_firebase_url(self): 6 | test_urls = [ 7 | 'https://validurl.firebaseio.com', 8 | 'https://anothervalidurl.firebaseio.com/', 9 | 'https://another-validurl98.firebaseio.com/' 10 | ] 11 | for url in test_urls: 12 | self.assertTrue(Firebase.is_valid_firebase_url(url)) 13 | --------------------------------------------------------------------------------