├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── firebase.py ├── requirements.txt └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Shariq Hashme 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebase-python 2 | 3 | 4 | This repo is now maintained by @lgozalvez 5 | 6 | 7 | 8 | ###### A nice, easy Python Firebase integration 9 | 10 | 11 | Supports streaming data. 12 | 13 | Requires `requests` and `sseclient`, which are on pip. If you don't know what that is, don't worry; just run `./setup.sh`. Made for Python 2.7. 14 | 15 | First create a Firebase here: 16 | https://www.firebase.com/signup/ 17 | 18 | (warning: The free level of Firebase only allows up to 50 concurrent connections. Don't hit this limit!) 19 | 20 | 21 | 22 | ## get and put 23 | 24 | `get` gets the value of a Firebase at some URL, `put` writes or replaces data at a Firebase path. 25 | 26 | ```python 27 | >>> import firebase 28 | >>> URL = 'lucid-lychee' # see note on URLs at the bottom of documentation 29 | >>> print firebase.get(URL) # this is an empty Firebase 30 | None 31 | 32 | >>> firebase.put(URL, 'tell me everything') # can take a string 33 | >>> print firebase.get(URL) 34 | tell me everything 35 | 36 | >>> firebase.put(URL, {'lucidity': 9001}) # or a dictionary 37 | >>> print firebase.get(URL) 38 | {u'lucidity': 9001} 39 | 40 | >>> firebase.put(URL, {'color': 'red'}) # replaces old value 41 | >>> print firebase.get(URL) 42 | {u'color': u'red'} 43 | 44 | >>> print firebase.get(URL + '/color') 45 | red 46 | ``` 47 | 48 | 49 | 50 | ## push 51 | 52 | `push` pushes data to a list on a Firebase path. This is the same as `patch`ing with an incrementing key, with Firebase taking care of concurrency issues. 53 | 54 | ```python 55 | >>> import firebase 56 | >>> URL = 'bickering-blancmanges' 57 | >>> print firebase.get(URL) 58 | None 59 | 60 | >>> firebase.push(URL, {'color': 'pink', 'jiggliness': 'high'}) 61 | >>> firebase.get(URL) 62 | { 63 | u'-JyAXHX9ZNBh7tPPja4w': {u'color': u'pink', u'jiggliness': u'high'} 64 | } 65 | 66 | >>> firebase.push(URL, {'color': 'white', 'jiggliness': 'extreme'}) 67 | >>> firebase.get(URL) 68 | { 69 | u'-JyAXHX9ZNBh7tPPja4w': {u'color': u'pink', u'jiggliness': u'high'}, 70 | u'-JyAXHX9ZNBh7tPPjasd': {u'color': u'white', u'jiggliness': u'extreme'} 71 | } 72 | ``` 73 | 74 | 75 | 76 | ## patch 77 | 78 | `patch` adds new key value pairs to an existing Firebase, without deleting the old key value pairs. 79 | 80 | ```python 81 | >>> import firebase 82 | >>> URL = 'tibetan-tumbleweed' 83 | >>> print firebase.get(URL) 84 | None 85 | 86 | >>> firebase.patch(URL, {'taste': 'tibetan'}) 87 | >>> print firebase.get(URL) 88 | {u'taste': u'tibetan'} 89 | 90 | >>> firebase.patch(URL, {'size': 'tumbly}) # patching does not overwrite 91 | >>> print firebase.get(URL) 92 | {u'taste': u'tibetan', u'size': u'tumbly'} 93 | ``` 94 | 95 | 96 | 97 | ## subscriber 98 | 99 | `subscriber` takes a URL and callback function and calls the callback on every update of the Firebase at URL. 100 | 101 | ```python 102 | >>> import firebase 103 | >>> from pprint import pprint # function which pretty prints objects 104 | >>> URL = 'clumsy-clementine' 105 | >>> S = firebase.subscriber(URL, pprint) # pprint will be called on all Firebase updates 106 | >>> S.start() # will get called with initial value of URL, which is empty 107 | (u'put', {u'data': None, u'path': u'/'}) 108 | 109 | >>> firebase.put(URL, ';-)') # will make S print something 110 | (u'put', {u'data': u';-)', u'path': u'/'}) 111 | 112 | >>> firebase.put(URL, {'status': 'mortified'}) # continuing from above 113 | (u'put', {u'data': {u'status': u'mortified'}, u'path': u'/'}) 114 | >>> firebase.patch(URL, {'reason': 'blushing'}) # same data, different method 115 | (u'patch', {u'data': {u'reason': u'blushing'}, u'path': u'/'}) 116 | 117 | >>> firebase.put(URL + '/color', 'red') 118 | (u'put', {u'data': u'red', u'path': u'/color'}) 119 | 120 | >>> S.stop() 121 | ``` 122 | 123 | 124 | 125 | ## URLs 126 | All URLs are internally converted to the apparent Firebase URL. This is done by the `firebaseURL` method. 127 | 128 | ```python 129 | >>> import firebase 130 | 131 | >>> print firebase.firebaseURL('bony-badger') 132 | https://bony-badger.firebaseio.com/.json 133 | 134 | >>> print firebase.firebaseURL('bony-badger/bones/humerus') 135 | https://bony-badger.firebaseio.com/bones/humerus.json 136 | 137 | >>> print firebase.firebaseURL('bony-badger.firebaseio.com/') 138 | https://bony-badger.firebaseio.com/.json 139 | ``` 140 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Add a state variable to the subscriber which 2 | the developer can access so they don't need 3 | to track state on their own 4 | -------------------------------------------------------------------------------- /firebase.py: -------------------------------------------------------------------------------- 1 | # adapted from firebase/EventSource-Examples/python/chat.py by Shariq Hashme 2 | 3 | from sseclient import SSEClient 4 | import requests 5 | 6 | from Queue import Queue 7 | import json 8 | import threading 9 | import socket 10 | 11 | 12 | class ClosableSSEClient(SSEClient): 13 | 14 | def __init__(self, *args, **kwargs): 15 | self.should_connect = True 16 | super(ClosableSSEClient, self).__init__(*args, **kwargs) 17 | 18 | def _connect(self): 19 | if self.should_connect: 20 | super(ClosableSSEClient, self)._connect() 21 | else: 22 | raise StopIteration() 23 | 24 | def close(self): 25 | self.should_connect = False 26 | self.retry = 0 27 | try: 28 | self.resp.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR) 29 | self.resp.raw._fp.fp._sock.close() 30 | except AttributeError: 31 | pass 32 | 33 | 34 | class RemoteThread(threading.Thread): 35 | 36 | def __init__(self, parent, URL, function): 37 | self.function = function 38 | self.URL = URL 39 | self.parent = parent 40 | super(RemoteThread, self).__init__() 41 | 42 | def run(self): 43 | try: 44 | self.sse = ClosableSSEClient(self.URL, chunk_size=32) 45 | for msg in self.sse: 46 | msg_data = json.loads(msg.data) 47 | if msg_data is None: # keep-alives 48 | continue 49 | msg_event = msg.event 50 | # TODO: update parent cache here 51 | self.function((msg.event, msg_data)) 52 | except socket.error: 53 | pass # this can happen when we close the stream 54 | except KeyboardInterrupt: 55 | self.close() 56 | 57 | def close(self): 58 | if self.sse: 59 | self.sse.close() 60 | 61 | 62 | def firebaseURL(URL): 63 | if '.firebaseio.com' not in URL.lower(): 64 | if '.json' == URL[-5:]: 65 | URL = URL[:-5] 66 | if '/' in URL: 67 | if '/' == URL[-1]: 68 | URL = URL[:-1] 69 | URL = 'https://' + \ 70 | URL.split('/')[0] + '.firebaseio.com/' + URL.split('/', 1)[1] + '.json' 71 | else: 72 | URL = 'https://' + URL + '.firebaseio.com/.json' 73 | return URL 74 | 75 | if 'http://' in URL: 76 | URL = URL.replace('http://', 'https://') 77 | if 'https://' not in URL: 78 | URL = 'https://' + URL 79 | if '.json' not in URL.lower(): 80 | if '/' != URL[-1]: 81 | URL = URL + '/.json' 82 | else: 83 | URL = URL + '.json' 84 | return URL 85 | 86 | 87 | class subscriber: 88 | 89 | def __init__(self, URL, function): 90 | self.cache = {} 91 | self.remote_thread = RemoteThread(self, firebaseURL(URL), function) 92 | 93 | def start(self): 94 | self.remote_thread.start() 95 | 96 | def stop(self): 97 | self.remote_thread.close() 98 | self.remote_thread.join() 99 | 100 | def wait(self): 101 | self.remote_thread.join() 102 | 103 | 104 | class FirebaseException(Exception): 105 | pass 106 | 107 | 108 | def put(URL, msg): 109 | to_post = json.dumps(msg) 110 | response = requests.put(firebaseURL(URL), data=to_post) 111 | if response.status_code != 200: 112 | raise FirebaseException(response.text) 113 | 114 | 115 | def patch(URL, msg): 116 | to_post = json.dumps(msg) 117 | response = requests.patch(firebaseURL(URL), data=to_post) 118 | if response.status_code != 200: 119 | raise FirebaseException(response.text) 120 | 121 | 122 | def get(URL): 123 | response = requests.get(firebaseURL(URL)) 124 | if response.status_code != 200: 125 | raise FirebaseException(response.text) 126 | return json.loads(response.text) 127 | 128 | 129 | def push(URL, msg): 130 | to_post = json.dumps(msg) 131 | response = requests.post(firebaseURL(URL), data=to_post) 132 | if response.status_code != 200: 133 | raise Exception(response.text) 134 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | sseclient 3 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install the basics on Debian based systems 4 | apt-get install build-essential python python-dev curl 5 | 6 | # install pip 7 | curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py 8 | python get-pip.py 9 | rm get-pip.py 10 | 11 | # install the Python modules listed in requirements.txt 12 | pip install -r requirements.txt 13 | --------------------------------------------------------------------------------