├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── CHANGES.txt ├── CONTRIBUTORS ├── INSTALL ├── LICENSE ├── README.md ├── README.rst ├── examples ├── __init__.py └── firebase_example.py ├── firebase ├── __init__.py ├── decorators.py ├── firebase.py ├── firebase_token_generator.py ├── jsonutil.py ├── lazy.py └── multiprocess_pool.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── firebase_test.py └── jsonutil_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # PyCharm 38 | .idea 39 | 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - 3.2 6 | install: 7 | - pip install -r requirements.txt --use-mirrors 8 | - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm 9 | script: python setup.py test 10 | notifications: 11 | - email: false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "restructuredtext.confPath": "" 3 | } -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | * 1.1 2 | - Compatibility fixes for Python 3.2 3 | - Relative imports fixes 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Thanks a lot to those people who contribute to the project by submitting 2 | patches and help me fix the bugs. 3 | 4 | * mumino 5 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | To install the package simply run the below command on your terminal: 2 | 3 | python setup.py install 4 | 5 | Don't forget to file bugs and let me know about them. 6 | Also, don't hasitate to ask for new features. Happy coding. 7 | 8 | Ozgur Vatansever -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2013 Ozgur Vatansever (ozgurvt@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Firebase 2 | 3 | Python interface to the Firebase's REST API 4 | 5 | [![Firebase](https://szimek.github.io/presentation-firebase-intro/images/firebase_logo.png)](http://www.firebase.com) 6 | 7 | ## Installation 8 | 9 | [![Build Status](https://travis-ci.org/ozgur/python-firebase.png?branch=master)](https://travis-ci.org/ozgur/python-firebase) 10 | 11 | python-firebase highly makes use of the **requests** library so before you begin, you need to have that package installed. 12 | 13 | $ sudo pip install requests 14 | $ sudo pip install python-firebase 15 | 16 | ## Getting Started 17 | 18 | You can fetch any of your data in JSON format by appending '.json' to the end of the URL in which your data resides and, then send an HTTPS request through your browser. Like all other REST specific APIs, Firebase offers a client to update(PATCH, PUT), create(POST), or remove(DELETE) his stored data along with just to fetch it. 19 | 20 | The library provides all the correspoding methods for those actions in both synchoronous and asynchronous manner. You can just start an asynchronous GET request with your callback function, and the method 21 | 22 | 23 | To fetch all the users in your storage simply do the following: 24 | 25 | ```python 26 | from firebase import firebase 27 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 28 | result = fb_app.get('/users', None) 29 | print result 30 | {'1': 'John Doe', '2': 'Jane Doe'} 31 | ``` 32 | 33 | 34 | The second argument of **get** method is the name of the snapshot. Thus, if you leave it NULL, you get the data in the URL **/users.json**. Besides, if you set it to **1**, you get the data in the url **/users/1.json**. In other words, you get the user whose ID equals to 1. 35 | 36 | ```python 37 | from firebase import firebase 38 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 39 | result = fb_app.get('/users', '1') 40 | print result 41 | {'1': 'John Doe'} 42 | ``` 43 | 44 | You can also provide extra query parameters that will be appended to the url or extra key-value pairs sent in the HTTP header. 45 | 46 | ```python 47 | from firebase import firebase 48 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 49 | result = fb_app.get('/users/2', None, {'print': 'pretty'}, {'X_FANCY_HEADER': 'VERY FANCY'}) 50 | print result 51 | {'2': 'Jane Doe'} 52 | ``` 53 | 54 | Creating new data requires a POST or PUT request. Assuming you don't append **print=silent** to the url, if you use POST the returning value becomes the name of the snapshot, if PUT you get the data you just sent. If print=silent is provided, you get just NULL because the backend never sends an output. 55 | 56 | ```python 57 | from firebase import firebase 58 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 59 | new_user = 'Ozgur Vatansever' 60 | 61 | result = fb_app.post('/users', new_user, {'print': 'pretty'}, {'X_FANCY_HEADER': 'VERY FANCY'}) 62 | print result 63 | {u'name': u'-Io26123nDHkfybDIGl7'} 64 | 65 | result = fb_app.post('/users', new_user, {'print': 'silent'}, {'X_FANCY_HEADER': 'VERY FANCY'}) 66 | print result == None 67 | True 68 | ``` 69 | 70 | Deleting data is relatively easy compared to other actions. You just set the url and that's all. Backend sends no output as a result of a delete operation. 71 | 72 | ```python 73 | from firebase import firebase 74 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 75 | fb_app.delete('/users', '1') 76 | # John Doe goes away. 77 | ``` 78 | 79 | ## Authentication 80 | 81 | Authentication in Firebase is nothing but to simply creating a token that conforms to the JWT standards and, putting it into the querystring with the name **auth**. The library creates that token for you so you never end up struggling with constructing a valid token on your own. If the data has been protected against write/read operations with some security rules, the backend sends an appropriate error message back to the client with the status code **403 Forbidden**. 82 | 83 | ```python 84 | from firebase import firebase 85 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', authentication=None) 86 | result = fb_app.get('/users', None, {'print': 'pretty'}) 87 | print result 88 | {'error': 'Permission denied.'} 89 | 90 | authentication = firebase.FirebaseAuthentication('THIS_IS_MY_SECRET', 'ozgurvt@gmail.com', extra={'id': 123}) 91 | fb_app.authentication = authentication 92 | print authentication.extra 93 | {'admin': False, 'debug': False, 'email': 'ozgurvt@gmail.com', 'id': 123, 'provider': 'password'} 94 | 95 | user = authentication.get_user() 96 | print user.firebase_auth_token 97 | "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJhZG1pbiI6IGZhbHNlLCAiZGVidWciOiBmYWxzZSwgIml 98 | hdCI6IDEzNjE5NTAxNzQsICJkIjogeyJkZWJ1ZyI6IGZhbHNlLCAiYWRtaW4iOiBmYWxzZSwgInByb3ZpZGVyIjog 99 | InBhc3N3b3JkIiwgImlkIjogNSwgImVtYWlsIjogIm96Z3VydnRAZ21haWwuY29tIn0sICJ2IjogMH0.lq4IRVfvE 100 | GQklslOlS4uIBLSSJj88YNrloWXvisRgfQ" 101 | 102 | result = fb_app.get('/users', None, {'print': 'pretty'}) 103 | print result 104 | {'1': 'John Doe', '2': 'Jane Doe'} 105 | ``` 106 | 107 | ## Concurrency 108 | 109 | The interface heavily depends on the standart **multiprocessing** library when concurrency comes in. While creating an asynchronous call, an on-demand process pool is created and, the async method is executed by one of the idle process inside the pool. The pool remains alive until the main process dies. So every time you trigger an async call, you always use the same pool. When the method returns, the pool process ships the returning value back to the main process within the callback function provided. 110 | 111 | ```python 112 | import json 113 | 114 | from firebase import firebase 115 | from firebase import jsonutil 116 | 117 | fb_app = firebase.FirebaseApplication('https://your_storage.firebaseio.com', authentication=None) 118 | 119 | def log_user(response): 120 | with open('/tmp/users/%s.json' % response.keys()[0], 'w') as users_file: 121 | users_file.write(json.dumps(response, cls=jsonutil.JSONEncoder)) 122 | 123 | fb_app.get_async('/users', None, {'print': 'pretty'}, callback=log_user) 124 | ``` 125 | 126 | # TODO 127 | 128 | - [ ] Async calls must deliver exceptions raised back to the main process. 129 | - [ ] More regression/stress tests on asynchronous calls. 130 | - [ ] Docs must be generated. 131 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python Firebase 2 | ================= 3 | 4 | Python interface to Firebase's REST API 5 | 6 | .. image:: https://travis-ci.org/ozgur/python-firebase.png?branch=master 7 | :target: https://travis-ci.org/ozgur/python-firebase 8 | 9 | Installation 10 | ----------------- 11 | 12 | python-firebase depends heavily on the **requests** library. 13 | 14 | .. code-block:: bash 15 | 16 | $ sudo pip install requests==1.1.0 17 | $ sudo pip install python-firebase 18 | 19 | Getting Started 20 | ------------------ 21 | 22 | You can read or write any of your data in JSON format. Append '.json' to the end of the URL in which your data resides. Send an HTTPS request from the browser. You can read (GET), replace (PUT), selectively update (PATCH), append (POST), or remove (DELETE) data From firebase. 23 | 24 | The library provides all the correspoding methods for those actions in both synchoronous and asynchronous manner. 25 | 26 | To read some data, start an asynchronous GET request with your callback function. For example, to fetch the entire content of "/users" in your Firebase database called "your_storage", do the following: 27 | 28 | .. code-block:: python 29 | 30 | from firebase import firebase 31 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 32 | result = firebase.get('/users', None) 33 | print result 34 | {'1': 'John Doe', '2': 'Jane Doe'} 35 | 36 | The second argument of **get** method is the branch of the database you wish to read. If you leave it None, you get all the data in the URL **/users.json**. If, instead, you set it to **1**, you get the data in the url **/users/1.json**. In other words, you get the user whose ID equals to 1. 37 | 38 | .. code-block:: python 39 | 40 | from firebase import firebase 41 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 42 | result = firebase.get('/users', '1') 43 | print result 44 | {'1': 'John Doe'} 45 | 46 | You can also provide extra query parameters that will be appended to the url or extra key-value pairs sent in the HTTP header. 47 | 48 | .. code-block:: python 49 | 50 | from firebase import firebase 51 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 52 | result = firebase.get('/users/2', None, {'print': 'pretty'}, {'X_FANCY_HEADER': 'VERY FANCY'}) 53 | print result 54 | {'2': 'Jane Doe'} 55 | 56 | Creating new data requires a PUT or POST request. If you know exactly where you want to put the data, use PUT. If you just want to append some data under a new key, but don't want to tell Firebase what key to use, use POST and Firebase will create a unique time-ordered key. 57 | 58 | By default, in POST the function returns the a dictionary containing in "name", the key it has created for the data you have written, and in PUT the function returns the data you have just sent. If, instead, you set print=silent, the function returns None because the backend never sends an output. 59 | 60 | .. code-block:: python 61 | 62 | from firebase import firebase 63 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 64 | new_user = 'Ozgur Vatansever' 65 | 66 | result = firebase.post('/users', new_user, {'print': 'pretty'}, {'X_FANCY_HEADER': 'VERY FANCY'}) 67 | print result 68 | {u'name': u'-Io26123nDHkfybDIGl7'} 69 | 70 | result = firebase.post('/users', new_user, {'print': 'silent'}, {'X_FANCY_HEADER': 'VERY FANCY'}) 71 | print result == None 72 | True 73 | 74 | Deleting data is relatively easy compared to other actions. You just specify the url. The backend sends no output. 75 | 76 | .. code-block:: python 77 | 78 | from firebase import firebase 79 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', None) 80 | firebase.delete('/users', '1') 81 | # John Doe goes away. 82 | 83 | Authentication 84 | ------------------ 85 | 86 | Authentication in Firebase involves simply creating a token that conforms to the JWT standarts and putting it into the querystring with the name **auth**. The library creates that token for you so you never end up struggling with constructing a valid token on your own. If the data has been protected against write/read operations with some security rules, the backend sends an appropriate error message back to the client with the status code **403 Forbidden**. 87 | 88 | .. code-block:: python 89 | 90 | from firebase import firebase 91 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', authentication=None) 92 | result = firebase.get('/users', None, {'print': 'pretty'}) 93 | print result 94 | {'error': 'Permission denied.'} 95 | 96 | authentication = firebase.FirebaseAuthentication('THIS_IS_MY_SECRET', 'ozgurvt@gmail.com', extra={'id': 123}) 97 | firebase.authentication = authentication 98 | print authentication.extra 99 | {'admin': False, 'debug': False, 'email': 'ozgurvt@gmail.com', 'id': 123, 'provider': 'password'} 100 | 101 | user = authentication.get_user() 102 | print user.firebase_auth_token 103 | "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJhZG1pbiI6IGZhbHNlLCAiZGVidWciOiBmYWxzZSwgIml 104 | hdCI6IDEzNjE5NTAxNzQsICJkIjogeyJkZWJ1ZyI6IGZhbHNlLCAiYWRtaW4iOiBmYWxzZSwgInByb3ZpZGVyIjog 105 | InBhc3N3b3JkIiwgImlkIjogNSwgImVtYWlsIjogIm96Z3VydnRAZ21haWwuY29tIn0sICJ2IjogMH0.lq4IRVfvE 106 | GQklslOlS4uIBLSSJj88YNrloWXvisRgfQ" 107 | 108 | result = firebase.get('/users', None, {'print': 'pretty'}) 109 | print result 110 | {'1': 'John Doe', '2': 'Jane Doe'} 111 | 112 | 113 | Concurrency 114 | ------------------ 115 | 116 | The interface heavily depends on the standart **multiprocessing** library when concurrency comes in. While creating an asynchronous call, an on-demand process pool is created and, the async method is executed by one of the idle process inside the pool. The pool remains alive until the main process dies. So every time you trigger an async call, you always use the same pool. When the method returns, the pool process ships the returning value back to the main process within the callback function provided. 117 | 118 | .. code-block:: python 119 | 120 | import json 121 | from firebase import firebase 122 | from firebase import jsonutil 123 | 124 | firebase = firebase.FirebaseApplication('https://your_storage.firebaseio.com', authentication=None) 125 | 126 | def log_user(response): 127 | with open('/tmp/users/%s.json' % response.keys()[0], 'w') as users_file: 128 | users_file.write(json.dumps(response, cls=jsonutil.JSONEncoder)) 129 | 130 | firebase.get_async('/users', None, {'print': 'pretty'}, callback=log_user) 131 | 132 | 133 | TODO 134 | --------- 135 | 136 | * Async calls must deliver exceptions raised back to the main process. 137 | * More regression/stress tests on asynchronous calls. 138 | * Docs must be generated. 139 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgur/python-firebase/fd76de2795a6611cabbde66944e1c6278e9135e2/examples/__init__.py -------------------------------------------------------------------------------- /examples/firebase_example.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from firebase.firebase import FirebaseApplication, FirebaseAuthentication 4 | 5 | 6 | if __name__ == '__main__': 7 | SECRET = '12345678901234567890' 8 | DSN = 'https://firebase.localhost' 9 | EMAIL = 'your@email.com' 10 | authentication = FirebaseAuthentication(SECRET,EMAIL, True, True) 11 | firebase = FirebaseApplication(DSN, authentication) 12 | 13 | firebase.get('/users', None, 14 | params={'print': 'pretty'}, 15 | headers={'X_FANCY_HEADER': 'very fancy'}) 16 | 17 | data = {'name': 'Ozgur Vatansever', 'age': 26, 18 | 'created_at': datetime.datetime.now()} 19 | 20 | snapshot = firebase.post('/users', data) 21 | print(snapshot['name']) 22 | 23 | def callback_get(response): 24 | with open('/dev/null', 'w') as f: 25 | f.write(response) 26 | firebase.get_async('/users', snapshot['name'], callback=callback_get) 27 | 28 | -------------------------------------------------------------------------------- /firebase/__init__.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | 3 | from .multiprocess_pool import process_pool 4 | from firebase import * 5 | 6 | 7 | @atexit.register 8 | def close_process_pool(): 9 | """ 10 | Clean up function that closes and terminates the process pool 11 | defined in the ``async`` file. 12 | """ 13 | process_pool.close() 14 | process_pool.join() 15 | process_pool.terminate() 16 | -------------------------------------------------------------------------------- /firebase/decorators.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from functools import wraps 3 | 4 | 5 | def http_connection(timeout): 6 | """ 7 | Decorator function that injects a requests.Session instance into 8 | the decorated function's actual parameters if not given. 9 | """ 10 | def wrapper(f): 11 | def wrapped(*args, **kwargs): 12 | if not ('connection' in kwargs) or not kwargs['connection']: 13 | connection = requests.Session() 14 | kwargs['connection'] = connection 15 | else: 16 | connection = kwargs['connection'] 17 | 18 | if not getattr(connection, 'timeout', False): 19 | connection.timeout = timeout 20 | connection.headers.update({'Content-type': 'application/json'}) 21 | return f(*args, **kwargs) 22 | return wraps(f)(wrapped) 23 | return wrapper 24 | -------------------------------------------------------------------------------- /firebase/firebase.py: -------------------------------------------------------------------------------- 1 | try: 2 | import urlparse 3 | except ImportError: 4 | #py3k 5 | from urllib import parse as urlparse 6 | 7 | import json 8 | 9 | from .firebase_token_generator import FirebaseTokenGenerator 10 | from .decorators import http_connection 11 | 12 | from .multiprocess_pool import process_pool 13 | from .jsonutil import JSONEncoder 14 | 15 | __all__ = ['FirebaseAuthentication', 'FirebaseApplication'] 16 | 17 | 18 | @http_connection(60) 19 | def make_get_request(url, params, headers, connection): 20 | """ 21 | Helper function that makes an HTTP GET request to the given firebase 22 | endpoint. Timeout is 60 seconds. 23 | `url`: The full URL of the firebase endpoint (DSN appended.) 24 | `params`: Python dict that is appended to the URL like a querystring. 25 | `headers`: Python dict. HTTP request headers. 26 | `connection`: Predefined HTTP connection instance. If not given, it 27 | is supplied by the `decorators.http_connection` function. 28 | 29 | The returning value is a Python dict deserialized by the JSON decoder. However, 30 | if the status code is not 2x or 403, an requests.HTTPError is raised. 31 | 32 | connection = connection_pool.get_available_connection() 33 | response = make_get_request('http://firebase.localhost/users', {'print': silent'}, 34 | {'X_FIREBASE_SOMETHING': 'Hi'}, connection) 35 | response => {'1': 'John Doe', '2': 'Jane Doe'} 36 | """ 37 | timeout = getattr(connection, 'timeout') 38 | response = connection.get(url, params=params, headers=headers, timeout=timeout) 39 | if response.ok or response.status_code == 403: 40 | return response.json() if response.content else None 41 | else: 42 | response.raise_for_status() 43 | 44 | 45 | @http_connection(60) 46 | def make_put_request(url, data, params, headers, connection): 47 | """ 48 | Helper function that makes an HTTP PUT request to the given firebase 49 | endpoint. Timeout is 60 seconds. 50 | `url`: The full URL of the firebase endpoint (DSN appended.) 51 | `data`: JSON serializable dict that will be stored in the remote storage. 52 | `params`: Python dict that is appended to the URL like a querystring. 53 | `headers`: Python dict. HTTP request headers. 54 | `connection`: Predefined HTTP connection instance. If not given, it 55 | is supplied by the `decorators.http_connection` function. 56 | 57 | The returning value is a Python dict deserialized by the JSON decoder. However, 58 | if the status code is not 2x or 403, an requests.HTTPError is raised. 59 | 60 | connection = connection_pool.get_available_connection() 61 | response = make_put_request('http://firebase.localhost/users', 62 | '{"1": "Ozgur Vatansever"}', 63 | {'X_FIREBASE_SOMETHING': 'Hi'}, connection) 64 | response => {'1': 'Ozgur Vatansever'} or {'error': 'Permission denied.'} 65 | """ 66 | timeout = getattr(connection, 'timeout') 67 | response = connection.put(url, data=data, params=params, headers=headers, 68 | timeout=timeout) 69 | if response.ok or response.status_code == 403: 70 | return response.json() if response.content else None 71 | else: 72 | response.raise_for_status() 73 | 74 | 75 | @http_connection(60) 76 | def make_post_request(url, data, params, headers, connection): 77 | """ 78 | Helper function that makes an HTTP POST request to the given firebase 79 | endpoint. Timeout is 60 seconds. 80 | `url`: The full URL of the firebase endpoint (DSN appended.) 81 | `data`: JSON serializable dict that will be stored in the remote storage. 82 | `params`: Python dict that is appended to the URL like a querystring. 83 | `headers`: Python dict. HTTP request headers. 84 | `connection`: Predefined HTTP connection instance. If not given, it 85 | is supplied by the `decorators.http_connection` function. 86 | 87 | The returning value is a Python dict deserialized by the JSON decoder. However, 88 | if the status code is not 2x or 403, an requests.HTTPError is raised. 89 | 90 | connection = connection_pool.get_available_connection() 91 | response = make_put_request('http://firebase.localhost/users/', 92 | '{"Ozgur Vatansever"}', {'X_FIREBASE_SOMETHING': 'Hi'}, connection) 93 | response => {u'name': u'-Inw6zol_2f5ThHwVcSe'} or {'error': 'Permission denied.'} 94 | """ 95 | timeout = getattr(connection, 'timeout') 96 | response = connection.post(url, data=data, params=params, headers=headers, 97 | timeout=timeout) 98 | if response.ok or response.status_code == 403: 99 | return response.json() if response.content else None 100 | else: 101 | response.raise_for_status() 102 | 103 | 104 | @http_connection(60) 105 | def make_patch_request(url, data, params, headers, connection): 106 | """ 107 | Helper function that makes an HTTP PATCH request to the given firebase 108 | endpoint. Timeout is 60 seconds. 109 | `url`: The full URL of the firebase endpoint (DSN appended.) 110 | `data`: JSON serializable dict that will be stored in the remote storage. 111 | `params`: Python dict that is appended to the URL like a querystring. 112 | `headers`: Python dict. HTTP request headers. 113 | `connection`: Predefined HTTP connection instance. If not given, it 114 | is supplied by the `decorators.http_connection` function. 115 | 116 | The returning value is a Python dict deserialized by the JSON decoder. However, 117 | if the status code is not 2x or 403, an requests.HTTPError is raised. 118 | 119 | connection = connection_pool.get_available_connection() 120 | response = make_put_request('http://firebase.localhost/users/1', 121 | '{"Ozgur Vatansever"}', {'X_FIREBASE_SOMETHING': 'Hi'}, connection) 122 | response => {'Ozgur Vatansever'} or {'error': 'Permission denied.'} 123 | """ 124 | timeout = getattr(connection, 'timeout') 125 | response = connection.patch(url, data=data, params=params, headers=headers, 126 | timeout=timeout) 127 | if response.ok or response.status_code == 403: 128 | return response.json() if response.content else None 129 | else: 130 | response.raise_for_status() 131 | 132 | 133 | @http_connection(60) 134 | def make_delete_request(url, params, headers, connection): 135 | """ 136 | Helper function that makes an HTTP DELETE request to the given firebase 137 | endpoint. Timeout is 60 seconds. 138 | `url`: The full URL of the firebase endpoint (DSN appended.) 139 | `params`: Python dict that is appended to the URL like a querystring. 140 | `headers`: Python dict. HTTP request headers. 141 | `connection`: Predefined HTTP connection instance. If not given, it 142 | is supplied by the `decorators.http_connection` function. 143 | 144 | The returning value is NULL. However, if the status code is not 2x or 403, 145 | an requests.HTTPError is raised. 146 | 147 | connection = connection_pool.get_available_connection() 148 | response = make_put_request('http://firebase.localhost/users/1', 149 | {'X_FIREBASE_SOMETHING': 'Hi'}, connection) 150 | response => NULL or {'error': 'Permission denied.'} 151 | """ 152 | timeout = getattr(connection, 'timeout') 153 | response = connection.delete(url, params=params, headers=headers, timeout=timeout) 154 | if response.ok or response.status_code == 403: 155 | return response.json() if response.content else None 156 | else: 157 | response.raise_for_status() 158 | 159 | 160 | class FirebaseUser(object): 161 | """ 162 | Class that wraps the credentials of the authenticated user. Think of 163 | this as a container that holds authentication related data. 164 | """ 165 | def __init__(self, email, firebase_auth_token, provider, id=None): 166 | self.email = email 167 | self.firebase_auth_token = firebase_auth_token 168 | self.provider = provider 169 | self.id = id 170 | 171 | 172 | class FirebaseAuthentication(object): 173 | """ 174 | Class that wraps the Firebase SimpleLogin mechanism. Actually this 175 | class does not trigger a connection, simply fakes the auth action. 176 | 177 | In addition, the provided email and password information is totally 178 | useless and they never appear in the ``auth`` variable at the server. 179 | """ 180 | def __init__(self, secret, email, debug=False, admin=False, extra=None): 181 | self.authenticator = FirebaseTokenGenerator(secret, debug, admin) 182 | self.email = email 183 | self.provider = 'password' 184 | self.extra = (extra or {}).copy() 185 | self.extra.update({'debug': debug, 'admin': admin, 186 | 'email': self.email, 'provider': self.provider}) 187 | 188 | def get_user(self): 189 | """ 190 | Method that gets the authenticated user. The returning user has 191 | the token, email and the provider data. 192 | """ 193 | token = self.authenticator.create_token(self.extra) 194 | user_id = self.extra.get('id') 195 | return FirebaseUser(self.email, token, self.provider, user_id) 196 | 197 | 198 | class FirebaseApplication(object): 199 | """ 200 | Class that actually connects with the Firebase backend via HTTP calls. 201 | It fully implements the RESTful specifications defined by Firebase. Data 202 | is transmitted as in JSON format in both ways. This class needs a DSN value 203 | that defines the base URL of the backend, and if needed, authentication 204 | credentials are accepted and then are taken into consideration while 205 | constructing HTTP requests. 206 | 207 | There are also the corresponding asynchronous versions of each HTTP method. 208 | The async calls make use of the on-demand process pool defined under the 209 | module `async`. 210 | 211 | auth = FirebaseAuthentication(FIREBASE_SECRET, 'firebase@firebase.com', 'fbpw') 212 | firebase = FirebaseApplication('https://firebase.localhost', auth) 213 | 214 | That's all there is. Then you start connecting with the backend: 215 | 216 | json_dict = firebase.get('/users', '1', {'print': 'pretty'}) 217 | print json_dict 218 | {'1': 'John Doe', '2': 'Jane Doe', ...} 219 | 220 | Async version is: 221 | firebase.get('/users', '1', {'print': 'pretty'}, callback=log_json_dict) 222 | 223 | The callback method is fed with the returning response. 224 | """ 225 | NAME_EXTENSION = '.json' 226 | URL_SEPERATOR = '/' 227 | 228 | def __init__(self, dsn, authentication=None): 229 | assert dsn.startswith('https://'), 'DSN must be a secure URL' 230 | self.dsn = dsn 231 | self.authentication = authentication 232 | 233 | def _build_endpoint_url(self, url, name=None): 234 | """ 235 | Method that constructs a full url with the given url and the 236 | snapshot name. 237 | 238 | Example: 239 | full_url = _build_endpoint_url('/users', '1') 240 | full_url => 'http://firebase.localhost/users/1.json' 241 | """ 242 | if not url.endswith(self.URL_SEPERATOR): 243 | url = url + self.URL_SEPERATOR 244 | if name is None: 245 | name = '' 246 | return f'{urlparse.urljoin(self.dsn, url)}{name}{self.NAME_EXTENSION}' 247 | 248 | def _authenticate(self, params, headers): 249 | """ 250 | Method that simply adjusts authentication credentials for the 251 | request. 252 | `params` is the querystring of the request. 253 | `headers` is the header of the request. 254 | 255 | If auth instance is not provided to this class, this method simply 256 | returns without doing anything. 257 | """ 258 | if self.authentication: 259 | user = self.authentication.get_user() 260 | params.update({'auth_token': user.firebase_auth_token}) 261 | headers.update(self.authentication.authenticator.HEADERS) 262 | 263 | @http_connection(60) 264 | def get(self, url, name, params=None, headers=None, connection=None): 265 | """ 266 | Synchronous GET request. 267 | """ 268 | if name is None: name = '' 269 | params = params or {} 270 | headers = headers or {} 271 | endpoint = self._build_endpoint_url(url, name) 272 | self._authenticate(params, headers) 273 | return make_get_request(endpoint, params, headers, connection=connection) 274 | 275 | def get_async(self, url, name, callback=None, params=None, headers=None): 276 | """ 277 | Asynchronous GET request with the process pool. 278 | """ 279 | if name is None: name = '' 280 | params = params or {} 281 | headers = headers or {} 282 | endpoint = self._build_endpoint_url(url, name) 283 | self._authenticate(params, headers) 284 | process_pool.apply_async(make_get_request, 285 | args=(endpoint, params, headers), callback=callback) 286 | 287 | @http_connection(60) 288 | def put(self, url, name, data, params=None, headers=None, connection=None): 289 | """ 290 | Synchronous PUT request. There will be no returning output from 291 | the server, because the request will be made with ``silent`` 292 | parameter. ``data`` must be a JSONable value. 293 | """ 294 | assert name, 'Snapshot name must be specified' 295 | params = params or {} 296 | headers = headers or {} 297 | endpoint = self._build_endpoint_url(url, name) 298 | self._authenticate(params, headers) 299 | data = json.dumps(data, cls=JSONEncoder) 300 | return make_put_request(endpoint, data, params, headers, 301 | connection=connection) 302 | 303 | def put_async(self, url, name, data, callback=None, params=None, headers=None): 304 | """ 305 | Asynchronous PUT request with the process pool. 306 | """ 307 | if name is None: name = '' 308 | params = params or {} 309 | headers = headers or {} 310 | endpoint = self._build_endpoint_url(url, name) 311 | self._authenticate(params, headers) 312 | data = json.dumps(data, cls=JSONEncoder) 313 | process_pool.apply_async(make_put_request, 314 | args=(endpoint, data, params, headers), 315 | callback=callback) 316 | 317 | @http_connection(60) 318 | def post(self, url, data, params=None, headers=None, connection=None): 319 | """ 320 | Synchronous POST request. ``data`` must be a JSONable value. 321 | """ 322 | params = params or {} 323 | headers = headers or {} 324 | endpoint = self._build_endpoint_url(url, None) 325 | self._authenticate(params, headers) 326 | data = json.dumps(data, cls=JSONEncoder) 327 | return make_post_request(endpoint, data, params, headers, 328 | connection=connection) 329 | 330 | def post_async(self, url, data, callback=None, params=None, headers=None): 331 | """ 332 | Asynchronous POST request with the process pool. 333 | """ 334 | params = params or {} 335 | headers = headers or {} 336 | endpoint = self._build_endpoint_url(url, None) 337 | self._authenticate(params, headers) 338 | data = json.dumps(data, cls=JSONEncoder) 339 | process_pool.apply_async(make_post_request, 340 | args=(endpoint, data, params, headers), 341 | callback=callback) 342 | 343 | @http_connection(60) 344 | def patch(self, url, data, params=None, headers=None, connection=None): 345 | """ 346 | Synchronous POST request. ``data`` must be a JSONable value. 347 | """ 348 | params = params or {} 349 | headers = headers or {} 350 | endpoint = self._build_endpoint_url(url, None) 351 | self._authenticate(params, headers) 352 | data = json.dumps(data, cls=JSONEncoder) 353 | return make_patch_request(endpoint, data, params, headers, 354 | connection=connection) 355 | 356 | def patch_async(self, url, data, callback=None, params=None, headers=None): 357 | """ 358 | Asynchronous PATCH request with the process pool. 359 | """ 360 | params = params or {} 361 | headers = headers or {} 362 | endpoint = self._build_endpoint_url(url, None) 363 | self._authenticate(params, headers) 364 | data = json.dumps(data, cls=JSONEncoder) 365 | process_pool.apply_async(make_patch_request, 366 | args=(endpoint, data, params, headers), 367 | callback=callback) 368 | 369 | @http_connection(60) 370 | def delete(self, url, name, params=None, headers=None, connection=None): 371 | """ 372 | Synchronous DELETE request. ``data`` must be a JSONable value. 373 | """ 374 | if not name: name = '' 375 | params = params or {} 376 | headers = headers or {} 377 | endpoint = self._build_endpoint_url(url, name) 378 | self._authenticate(params, headers) 379 | return make_delete_request(endpoint, params, headers, connection=connection) 380 | 381 | def delete_async(self, url, name, callback=None, params=None, headers=None): 382 | """ 383 | Asynchronous DELETE request with the process pool. 384 | """ 385 | if not name: name = '' 386 | params = params or {} 387 | headers = headers or {} 388 | endpoint = self._build_endpoint_url(url, name) 389 | self._authenticate(params, headers) 390 | process_pool.apply_async(make_delete_request, 391 | args=(endpoint, params, headers), callback=callback) 392 | -------------------------------------------------------------------------------- /firebase/firebase_token_generator.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # THE ENTIRE CODE HAS BEEN TAKEN FROM THE OFFICIAL FIREBASE GITHUB # 3 | # REPOSITORY NAMED `firebase-token-generator-python` WITH SLIGHT # 4 | # MODIFICATIONS. # 5 | # # 6 | # FOR MORE INFORMATION, PLEASE TAKE A LOOK AT THE ACTUAL REPOSITORY: # 7 | # - https://github.com/firebase/firebase-token-generator-python # 8 | ############################################################################## 9 | import base64 10 | import hashlib 11 | import hmac 12 | import json 13 | import time 14 | 15 | __all__ = ['FirebaseTokenGenerator'] 16 | 17 | 18 | class FirebaseTokenGenerator(object): 19 | TOKEN_VERSION = 0 20 | TOKEN_SEP = '.' 21 | CLAIMS_MAP = { 22 | 'expires': 'exp', 23 | 'notBefore': 'nbf', 24 | 'admin': 'admin', 25 | 'debug': 'debug', 26 | 'simulate': 'simulate' 27 | } 28 | HEADERS = {'typ': 'JWT', 'alg': 'HS256'} 29 | 30 | def __init__(self, secret, debug=False, admin=False): 31 | assert secret, 'Your Firebase SECRET is not valid' 32 | self.secret = secret 33 | self.admin = admin 34 | self.debug = debug 35 | 36 | def create_token(self, data, options=None): 37 | """ 38 | Generates a secure authentication token. 39 | 40 | Our token format follows the JSON Web Token (JWT) standard: 41 | header.claims.signature 42 | 43 | Where: 44 | 1) 'header' is a stringified, base64-encoded JSON object containing version and algorithm information. 45 | 2) 'claims' is a stringified, base64-encoded JSON object containing a set of claims: 46 | Library-generated claims: 47 | 'iat' -> The issued at time in seconds since the epoch as a number 48 | 'd' -> The arbitrary JSON object supplied by the user. 49 | User-supplied claims (these are all optional): 50 | 'exp' (optional) -> The expiration time of this token, as a number of seconds since the epoch. 51 | 'nbf' (optional) -> The 'not before' time before which the token should be rejected (seconds since the epoch) 52 | 'admin' (optional) -> If set to true, this client will bypass all security rules (use this to authenticate servers) 53 | 'debug' (optional) -> 'set to true to make this client receive debug information about security rule execution. 54 | 'simulate' (optional, internal-only for now) -> Set to true to neuter all API operations (listens / puts 55 | will run security rules but not actually write or return data). 56 | 3) A signature that proves the validity of this token (see: http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-07) 57 | 58 | For base64-encoding we use URL-safe base64 encoding. This ensures that the entire token is URL-safe 59 | and could, for instance, be placed as a query argument without any encoding (and this is what the JWT spec requires). 60 | 61 | Args: 62 | data - a json serializable object of data to be included in the token 63 | options - An optional dictionary of additional claims for the token. Possible keys include: 64 | a) 'expires' -- A timestamp (as a number of seconds since the epoch) denoting a time after which 65 | this token should no longer be valid. 66 | b) 'notBefore' -- A timestamp (as a number of seconds since the epoch) denoting a time before 67 | which this token should be rejected by the server. 68 | c) 'admin' -- Set to true to bypass all security rules (use this for your trusted servers). 69 | d) 'debug' -- Set to true to enable debug mode (so you can see the results of Rules API operations) 70 | e) 'simulate' -- (internal-only for now) Set to true to neuter all API operations (listens / puts 71 | will run security rules but not actually write or return data) 72 | Returns: 73 | A signed Firebase Authentication Token 74 | Raises: 75 | ValueError: if an invalid key is specified in options 76 | """ 77 | if not options: 78 | options = {} 79 | options.update({'admin': self.admin, 'debug': self.debug}) 80 | claims = self._create_options_claims(options) 81 | claims['v'] = self.TOKEN_VERSION 82 | claims['iat'] = int(time.mktime(time.gmtime())) 83 | claims['d'] = data 84 | return self._encode_token(self.secret, claims) 85 | 86 | def _create_options_claims(self, opts): 87 | claims = {} 88 | for k in opts: 89 | if k in self.CLAIMS_MAP: 90 | claims[k] = opts[k] 91 | else: 92 | raise ValueError('Unrecognized Option: %s' % k) 93 | return claims 94 | 95 | def _encode(self, bytes): 96 | encoded = base64.urlsafe_b64encode(bytes) 97 | return encoded.decode('utf-8').replace('=', '') 98 | 99 | def _encode_json(self, obj): 100 | return self._encode(json.dumps(obj).encode("utf-8")) 101 | 102 | def _sign(self, secret, to_sign): 103 | def portable_bytes(s): 104 | try: 105 | return bytes(s, 'utf-8') 106 | except TypeError: 107 | return bytes(s) 108 | return self._encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), 109 | hashlib.sha256).digest()) 110 | 111 | def _encode_token(self, secret, claims): 112 | encoded_header = self._encode_json(self.HEADERS) 113 | encoded_claims = self._encode_json(claims) 114 | secure_bits = '%s%s%s' % (encoded_header, self.TOKEN_SEP, encoded_claims) 115 | sig = self._sign(secret, secure_bits) 116 | return '%s%s%s' % (secure_bits, self.TOKEN_SEP, sig) 117 | -------------------------------------------------------------------------------- /firebase/jsonutil.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import decimal 4 | 5 | try: 6 | total_seconds = datetime.timedelta.total_seconds 7 | except AttributeError: 8 | total_seconds = lambda self: ((self.days * 86400 + self.seconds) * 10 ** 6 + self.microseconds) / 10 ** 6.0 9 | 10 | 11 | class JSONEncoder(json.JSONEncoder): 12 | def default(self, obj): 13 | if isinstance(obj, (datetime.datetime, datetime.date)): 14 | return obj.isoformat() 15 | elif isinstance(obj, datetime.timedelta): 16 | return total_seconds(obj) 17 | elif isinstance(obj, decimal.Decimal): 18 | return float(obj) 19 | else: 20 | return json.JSONEncoder.default(self, obj) 21 | -------------------------------------------------------------------------------- /firebase/lazy.py: -------------------------------------------------------------------------------- 1 | 2 | class LazyLoadProxy(object): 3 | # Taken from http://code.activestate.com/recipes/496741-object-proxying/ 4 | __slots__ = ["_obj_fn", "__weakref__", "__proxy_storage"] 5 | def __init__(self, fn, storage=None): 6 | object.__setattr__(self, "_obj_fn", fn) 7 | object.__setattr__(self, "__proxy_storage", storage) 8 | 9 | def __getattribute__(self, name): 10 | return getattr(object.__getattribute__(self, "_obj_fn")(), name) 11 | def __delattr__(self, name): 12 | delattr(object.__getattribute__(self, "_obj_fn")(), name) 13 | def __setattr__(self, name, value): 14 | setattr(object.__getattribute__(self, "_obj_fn")(), name, value) 15 | def __getitem__(self, index): 16 | return object.__getattribute__(self, "_obj_fn")().__getitem__(index) 17 | def __nonzero__(self): 18 | return bool(object.__getattribute__(self, "_obj_fn")()) 19 | def __str__(self): 20 | return str(object.__getattribute__(self, "_obj_fn")()) 21 | def __repr__(self): 22 | return repr(object.__getattribute__(self, "_obj_fn")()) 23 | def __len__(self): 24 | return len(object.__getattribute__(self, "_obj_fn")()) 25 | 26 | _special_names = [ 27 | '__abs__', '__add__', '__and__', '__call__', '__cmp__', '__coerce__', 28 | '__contains__', '__delitem__', '__delslice__', '__div__', '__divmod__', 29 | '__eq__', '__float__', '__floordiv__', '__ge__', #'__getitem__', 30 | '__getslice__', '__gt__', '__hash__', '__hex__', '__iadd__', '__iand__', 31 | '__idiv__', '__idivmod__', '__ifloordiv__', '__ilshift__', '__imod__', 32 | '__imul__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', 33 | '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', #'__len__', 34 | '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', 35 | '__neg__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', 36 | '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', 37 | '__repr__', '__reversed__', '__rfloorfiv__', '__rlshift__', '__rmod__', 38 | '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', 39 | '__rtruediv__', '__rxor__', '__setitem__', '__setslice__', '__sub__', 40 | '__truediv__', '__xor__', 'next', 41 | ] 42 | 43 | @classmethod 44 | def _create_class_proxy(cls, theclass): 45 | """creates a proxy for the given class""" 46 | 47 | def make_method(name): 48 | def method(self, *args, **kw): 49 | return getattr(object.__getattribute__(self, "_obj_fn")(), name)( 50 | *args, **kw) 51 | return method 52 | 53 | namespace = {} 54 | for name in cls._special_names: 55 | if hasattr(theclass, name): 56 | namespace[name] = make_method(name) 57 | return type("%s(%s)" % (cls.__name__, theclass.__name__), (cls,), namespace) 58 | 59 | def __new__(cls, obj, *args, **kwargs): 60 | """ 61 | creates an proxy instance referencing `obj`. (obj, *args, **kwargs) are 62 | passed to this class' __init__, so deriving classes can define an 63 | __init__ method of their own. 64 | note: _class_proxy_cache is unique per deriving class (each deriving 65 | class must hold its own cache) 66 | """ 67 | try: 68 | cache = cls.__dict__["_class_proxy_cache"] 69 | except KeyError: 70 | cls._class_proxy_cache = cache = {} 71 | try: 72 | theclass = cache[obj.__class__] 73 | except KeyError: 74 | cache[obj.__class__] = theclass = cls._create_class_proxy(obj.__class__) 75 | ins = object.__new__(theclass) 76 | theclass.__init__(ins, obj, *args, **kwargs) 77 | return ins 78 | 79 | 80 | class Proxy(LazyLoadProxy): 81 | # Taken from http://code.activestate.com/recipes/496741-object-proxying/ 82 | def __init__(self, obj): 83 | super(Proxy, self).__init__(lambda: obj) 84 | -------------------------------------------------------------------------------- /firebase/multiprocess_pool.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | from .lazy import LazyLoadProxy 4 | 5 | __all__ = ['process_pool'] 6 | 7 | _process_pool = None 8 | def get_process_pool(size=5): 9 | global _process_pool 10 | if _process_pool is None: 11 | _process_pool = multiprocessing.Pool(processes=size) 12 | return _process_pool 13 | process_pool = LazyLoadProxy(get_process_pool) 14 | 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=1.1.0 2 | -------------------------------------------------------------------------------- /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 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 10 | long_description = readme.read() 11 | 12 | 13 | setup(name='python-firebase', 14 | version='1.2.1', 15 | description="Python interface to the Firebase's REST API.", 16 | long_description=long_description, 17 | classifiers=[ 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Environment :: Console', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | 'Programming Language :: Python :: 2.6', 24 | 'Programming Language :: Python :: 2.7', 25 | 'Programming Language :: Python :: 3.2', 26 | 'Natural Language :: English', 27 | ], 28 | keywords='firebase python', 29 | author='Ozgur Vatansever', 30 | author_email='ozgurvt@gmail.com', 31 | maintainer='Ozgur Vatansever', 32 | maintainer_email='ozgurvt@gmail.com', 33 | url='http://ozgur.github.com/python-firebase/', 34 | license='MIT', 35 | packages=['firebase'], 36 | test_suite='tests.all_tests', 37 | install_requires=['requests>=1.1.0'], 38 | zip_safe=False, 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .jsonutil_test import JSONTestCase 4 | from .firebase_test import FirebaseTestCase 5 | 6 | 7 | def all_tests(): 8 | suite = unittest.TestSuite() 9 | suite.addTest(unittest.makeSuite(JSONTestCase)) 10 | suite.addTest(unittest.makeSuite(FirebaseTestCase)) 11 | return suite 12 | -------------------------------------------------------------------------------- /tests/firebase_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import requests 4 | import json 5 | 6 | from firebase.firebase import (FirebaseAuthentication, FirebaseApplication, 7 | make_get_request, make_post_request, make_put_request, 8 | make_patch_request, make_delete_request) 9 | 10 | 11 | class MockConnection(object): 12 | def __init__(self, response): 13 | self.response = response 14 | self.headers = {} 15 | 16 | def get(self, url, params, headers, *args, **kwargs): 17 | return self.response 18 | 19 | def post(self, url, data, params, headers, *args, **kwargs): 20 | return self.response 21 | 22 | def put(self, url, data, params, headers, *args, **kwargs): 23 | return self.response 24 | 25 | def patch(self, url, data, params, headers, *args, **kwargs): 26 | return self.response 27 | 28 | def delete(self, url, params, headers, *args, **kwargs): 29 | return self.response 30 | 31 | 32 | class MockResponse(object): 33 | def __init__(self, status_code, content): 34 | self.status_code = status_code 35 | self.content = content 36 | 37 | @property 38 | def ok(self): 39 | return str(self.status_code).startswith('2') 40 | 41 | def json(self): 42 | if self.content: 43 | return json.loads(self.content) 44 | return None 45 | 46 | def raise_for_status(self): 47 | raise Exception('Fake HTTP Error') 48 | 49 | 50 | class FirebaseTestCase(unittest.TestCase): 51 | def setUp(self): 52 | self.SECRET = 'FAKE_FIREBASE_SECRET' 53 | self.DSN = 'https://firebase.localhost' 54 | self.EMAIL = 'python-firebase@firebase.com' 55 | self.authentication = FirebaseAuthentication(self.SECRET, self.EMAIL, 56 | None) 57 | self.firebase = FirebaseApplication(self.DSN, self.authentication) 58 | 59 | def test_build_endpoint_url(self): 60 | url1 = os.path.join(self.DSN, 'users', '1.json') 61 | self.assertEqual(self.firebase._build_endpoint_url('/users', '1'), url1) 62 | url2 = os.path.join(self.DSN, 'users/1/.json') 63 | self.assertEqual(self.firebase._build_endpoint_url('/users/1', None), url2) 64 | 65 | def test_make_get_request(self): 66 | response = MockResponse(403, json.dumps({'error': 'Permission required.'})) 67 | connection = MockConnection(response) 68 | result = self.firebase.get('url', 'shapshot', params={}, headers={}, 69 | connection=connection) 70 | self.assertEqual(result, json.loads(response.content)) 71 | 72 | def test_make_post_request(self): 73 | response = MockResponse(403, json.dumps({'error': 'Permission required.'})) 74 | connection = MockConnection(response) 75 | result = self.firebase.post('url', {}, params={}, headers={}, 76 | connection=connection) 77 | self.assertEqual(result, json.loads(response.content)) 78 | 79 | def test_make_put_request(self): 80 | response = MockResponse(403, json.dumps({'error': 'Permission required.'})) 81 | connection = MockConnection(response) 82 | result = self.firebase.put('url', 'snapshot', {}, params={}, headers={}, 83 | connection=connection) 84 | self.assertEqual(result, json.loads(response.content)) 85 | 86 | def test_make_patch_request(self): 87 | response = MockResponse(403, json.dumps({'error': 'Permission required.'})) 88 | connection = MockConnection(response) 89 | result = self.firebase.patch('url', {}, params={}, headers={}, 90 | connection=connection) 91 | self.assertEqual(result, json.loads(response.content)) 92 | 93 | def test_make_delete_request(self): 94 | response = MockResponse(403, json.dumps({'error': 'Permission required.'})) 95 | connection = MockConnection(response) 96 | result = self.firebase.delete('url', 'snapshot', params={}, headers={}, 97 | connection=connection) 98 | self.assertEqual(result, json.loads(response.content)) 99 | -------------------------------------------------------------------------------- /tests/jsonutil_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime 3 | import decimal 4 | import json 5 | 6 | from firebase.jsonutil import JSONEncoder 7 | 8 | 9 | class JSONTestCase(unittest.TestCase): 10 | def setUp(self): 11 | self.data = {'now': datetime.datetime.now(), 12 | 'oneday': datetime.timedelta(days=1), 13 | 'five': decimal.Decimal(5), 14 | 'date': datetime.date(2014, 3, 11)} 15 | 16 | def test_conversion(self): 17 | serialized = json.dumps(self.data, cls=JSONEncoder) 18 | deserialized = json.loads(serialized) 19 | self.assertEqual(deserialized['oneday'], 86400) 20 | self.assertTrue(type(deserialized['five']) == float) 21 | self.assertEqual(deserialized['five'], float(5)) 22 | self.assertEqual(deserialized['now'], str(self.data['now'].isoformat())) 23 | self.assertEqual(deserialized['date'], str(self.data['date'].isoformat())) 24 | 25 | def test_total_seconds(self): 26 | from firebase.jsonutil import total_seconds 27 | 28 | delta = datetime.timedelta(days=1, 29 | seconds=3, 30 | microseconds=440000, 31 | milliseconds=3300, 32 | minutes=5, 33 | hours=2, 34 | weeks=2) 35 | 36 | self.assertEqual(total_seconds(delta), 1303506.74) 37 | 38 | --------------------------------------------------------------------------------