├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── docs.yml │ ├── feature-request.yml │ └── others.yml └── workflows │ ├── build.yml │ ├── greetings.yml │ ├── publish.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── _static │ └── css │ │ └── my-styles.css ├── conf.py ├── firebase │ ├── firebase.auth.rst │ ├── firebase.database.rst │ ├── firebase.firestore.rst │ ├── firebase.rst │ ├── firebase.storage.rst │ └── modules.rst ├── guide │ ├── authentication.rst │ ├── database.rst │ ├── firebase-rest-api.rst │ ├── firestore.rst │ ├── setup.rst │ └── storage.rst ├── index.rst └── requirements.txt ├── firebase ├── __init__.py ├── _custom_requests.py ├── _exception.py ├── _service_account_credentials.py ├── auth │ └── __init__.py ├── database │ ├── __init__.py │ ├── _closable_sse_client.py │ ├── _custom_sse_client.py │ ├── _db_convert.py │ ├── _keep_auth_session.py │ └── _stream.py ├── firestore │ ├── __init__.py │ └── _utils.py └── storage │ └── __init__.py ├── pyproject.toml ├── requirements.txt └── tests ├── __init__.py ├── config.py ├── config.template.py ├── conftest.py ├── requirements.txt ├── static └── test-file.txt ├── test_auth.py ├── test_database.py ├── test_firestore.py ├── test_setup.py ├── test_storage.py └── tools.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🪲 Bug 2 | description: Report an issue to help improve the project. 3 | title: "[Bug]: " 4 | labels: ['bug', 'triage'] 5 | 6 | body: 7 | 8 | - type: markdown 9 | id: greet 10 | attributes: 11 | value: | 12 | Thanks for stopping by to let us know something could be better! 13 | 14 | - type: checkboxes 15 | id: quick-fix 16 | attributes: 17 | label: Is there an existing issue for this? 18 | description: | 19 | Please search to see if an issue already exists for the bug you encountered. 20 | 21 | - https://github.com/AsifArmanRahman/firebase-rest-api/issues 22 | options: 23 | - label: I have searched the existing issues 24 | required: true 25 | 26 | - type: markdown 27 | id: issue-persists 28 | attributes: 29 | value: | 30 | <br> 31 | 32 | ## If you are still having issues, please be sure to include as much information as possible: 33 | 34 | - type: textarea 35 | id: environment 36 | attributes: 37 | label: Environment 38 | description: | 39 | examples: 40 | - **OS**: Ubuntu 20.04 41 | - **Python**: 3.9.12 42 | value: | 43 | - OS: 44 | - Python: 45 | render: markdown 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: what-happened 51 | attributes: 52 | label: What happened? 53 | description: | 54 | Also tell us, what did you expect to happen? 55 | placeholder: | 56 | A bug happened! 57 | validations: 58 | required: true 59 | 60 | - type: textarea 61 | id: code-snippet 62 | attributes: 63 | label: Code Snippet 64 | description: | 65 | Please provide the code snippet to reproduce bug. 66 | 67 | Note: This will be automatically formatted into code, so no need for backtick's. 68 | render: shell 69 | validations: 70 | required: true 71 | 72 | - type: textarea 73 | id: logs 74 | attributes: 75 | label: Relevant log output 76 | description: | 77 | Please copy and paste any relevant log output. 78 | 79 | Note: This will be automatically formatted into code, so no need for backtick's. 80 | render: shell 81 | validations: 82 | required: false 83 | 84 | - type: textarea 85 | attributes: 86 | label: Anything else? 87 | description: | 88 | Links? References? Anything that will give us more context about the issue you are encountering! 89 | 90 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 91 | validations: 92 | required: false 93 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Discussion (Q&A) 5 | url: https://github.com/AsifArmanRahman/firebase-rest-api/discussions/categories/q-a 6 | about: Please ask and answer questions here. 7 | 8 | - name: GitHub Security Bug Bounty 9 | url: https://bounty.github.com/ 10 | about: Please report security vulnerabilities here. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yml: -------------------------------------------------------------------------------- 1 | name: 📄 Docs 2 | description: Found an issue in the documentation? You can use this one! 3 | title: "[Docs]: <title>" 4 | labels: ['documentation'] 5 | 6 | body: 7 | 8 | - type: markdown 9 | id: greet 10 | attributes: 11 | value: | 12 | Thanks for stopping by to let us know something could be better! 13 | <br> 14 | 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: Description 19 | description: | 20 | A brief description of the question or issue, also include what you tried and what didn't work. 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: screenshots 26 | attributes: 27 | label: Screenshots 28 | description: | 29 | Please add screenshots if applicable. 30 | 31 | Tip: You can attach images or files by clicking this area to highlight it and then dragging files in. 32 | validations: 33 | required: false 34 | 35 | - type: textarea 36 | id: extra_info 37 | attributes: 38 | label: Additional information 39 | description: | 40 | Is there anything else we should know about this issue? 41 | validations: 42 | required: false 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest an idea for this library. 3 | title: "[Feature]: <title>" 4 | labels: ['enhancement'] 5 | 6 | body: 7 | 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for stopping by to let us know something could be better! 12 | 13 | Please complete the below form to ensure we have all the details to get things started. 14 | <br> 15 | 16 | - type: textarea 17 | attributes: 18 | label: Is your proposal related to a problem? 19 | description: | 20 | Provide a clear and concise description of what the problem is. 21 | placeholder: | 22 | I'm always frustrated when... 23 | render: markdown 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | attributes: 29 | label: Describe the solution you'd like. 30 | description: | 31 | Provide a clear and concise description of what you want to happen. 32 | render: markdown 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Describe alternatives you've considered. 39 | description: | 40 | Let us know about other solutions you've tried or researched. 41 | render: markdown 42 | validations: 43 | required: false 44 | 45 | - type: textarea 46 | attributes: 47 | label: Additional context. 48 | description: | 49 | Is there anything else you can add about the proposal? 50 | You might want to link to related issues here, if you haven't already. 51 | render: markdown 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/others.yml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Use this for any other issues. 3 | title: "[Other]: <title>" 4 | 5 | body: 6 | - type: markdown 7 | id: greet 8 | attributes: 9 | value: | 10 | Thanks for stopping by to let us know something could be better! 11 | <br> 12 | 13 | - type: textarea 14 | id: issue_description 15 | attributes: 16 | label: What would you like to share? 17 | description: Provide a clear and concise explanation of your issue. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: extrainfo 23 | attributes: 24 | label: Additional information 25 | description: Is there anything else we should know about this issue? 26 | validations: 27 | required: false 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the "main" branch 6 | push: 7 | branches: [ "main" ] 8 | paths-ignore: 9 | - 'docs/**' 10 | pull_request: 11 | branches: [ "main" ] 12 | paths-ignore: 13 | - 'docs/**' 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | 21 | # This workflow contains a single job called "build" 22 | build: 23 | 24 | # The type of runner that the job will run on 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] 29 | os: [ macos-latest, ubuntu-latest, windows-latest ] 30 | 31 | # Steps represent a sequence of tasks that will be executed as part of the job 32 | steps: 33 | 34 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 35 | - uses: actions/checkout@v3 36 | 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v3 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | cache: pip 42 | 43 | - name: Install dependencies 44 | run: | 45 | python -m pip install --upgrade pip 46 | pip install -r requirements.txt 47 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: greetings 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | issue_comment: 9 | types: 10 | - created 11 | 12 | pull_request_target: 13 | types: 14 | - opened 15 | 16 | pull_request_review_comment: 17 | types: 18 | - created 19 | 20 | jobs: 21 | 22 | greeting: 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | issues: write 27 | pull-requests: write 28 | 29 | steps: 30 | 31 | - uses: actions/first-interaction@v1 32 | with: 33 | repo-token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | issue-message: | 36 | Hello @${{ github.actor }} , thank you for submitting an issue! A project committer will shortly review the issue. 37 | 38 | pr-message: | 39 | Thank you @${{ github.actor }} , for contributing to this project, your support is much appreciated. A project committer will shortly review your contribution. 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | 10 | publish: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | environment: 15 | name: Production 16 | 17 | steps: 18 | 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Python 3.9 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: 3.9 25 | cache: pip 26 | 27 | - name: To PyPI using Flit 28 | uses: AsifArmanRahman/to-pypi-using-flit@v1 29 | with: 30 | password: ${{ secrets.PYPI_API_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [ "tests" ] 6 | branches: [ "main" ] 7 | types: 8 | - completed 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | github-release: 15 | runs-on: ubuntu-latest 16 | 17 | # Workflow will run only if the trigger workflow completed with success. 18 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 19 | 20 | environment: 21 | name: Staging 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: conventional Changelog Action 27 | id: changelog 28 | uses: TriPSs/conventional-changelog-action@v3.7.1 29 | with: 30 | github-token: ${{ secrets.CHANGELOG_RELEASE }} 31 | git-user-name: Google GitHub Actions Bot 32 | git-user-email: 72759630+google-github-actions-bot@users.noreply.github.com 33 | git-message: 'release: {version}' 34 | version-file: ./pyproject.toml 35 | version-path: project.version 36 | release-count: 0 37 | 38 | - name: create release 39 | uses: actions/create-release@v1 40 | if: ${{ steps.changelog.outputs.skipped == 'false' }} 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.CHANGELOG_RELEASE }} 43 | with: 44 | tag_name: ${{ steps.changelog.outputs.tag }} 45 | release_name: ${{ steps.changelog.outputs.tag }} 46 | body: ${{ steps.changelog.outputs.clean_changelog }} 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow after 'Build' workflow completes running 6 | workflow_run: 7 | workflows: [ "Build" ] 8 | branches: [ "main" ] 9 | types: 10 | - completed 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | 18 | # This workflow contains a single job called "tests" 19 | on_success: 20 | 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | 24 | # Workflow will run only if the trigger workflow completed with success. 25 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 26 | environment: 27 | name: Development 28 | 29 | # Steps represent a sequence of tasks that will be executed as part of the job 30 | steps: 31 | 32 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 33 | - uses: actions/checkout@v3 34 | 35 | - name: Set up Python 3.9 36 | uses: actions/setup-python@v3 37 | with: 38 | python-version: 3.9 39 | cache: pip 40 | 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | pip install -r tests/requirements.txt 45 | pip install -r requirements.txt 46 | 47 | - name: Test with pytest and Generate Coverage report 48 | run: | 49 | pytest --cov=firebase --cov-report=xml:cov.xml 50 | env: 51 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} 52 | FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }} 53 | FIREBASE_DATABASE_URL: ${{ secrets.FIREBASE_DATABASE_URL }} 54 | FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} 55 | FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }} 56 | 57 | FIREBASE_SERVICE_ACCOUNT_PROJECT_ID: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROJECT_ID }} 58 | FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY_ID: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY_ID }} 59 | FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY }} 60 | FIREBASE_SERVICE_ACCOUNT_CLIENT_EMAIL: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CLIENT_EMAIL }} 61 | FIREBASE_SERVICE_ACCOUNT_CLIENT_ID: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CLIENT_ID }} 62 | FIREBASE_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL }} 63 | 64 | TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} 65 | TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} 66 | 67 | TEST_USER_EMAIL_2: ${{ secrets.TEST_USER_EMAIL_2 }} 68 | TEST_USER_PASSWORD_2: ${{ secrets.TEST_USER_PASSWORD_2 }} 69 | 70 | - name: Upload coverage to Codecov 71 | uses: codecov/codecov-action@v2 72 | with: 73 | token: ${{ secrets.CODECOV_TOKEN }} 74 | fail_ci_if_error: true 75 | files: cov.xml 76 | flags: pytest 77 | 78 | on_faliure: 79 | 80 | # The type of runner that the job will run on 81 | runs-on: ubuntu-latest 82 | 83 | # Workflow will run only if the trigger workflow completed with failure. 84 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 85 | environment: 86 | name: Development 87 | 88 | # Steps represent a sequence of tasks that will be executed as part of the job 89 | steps: 90 | 91 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 92 | - uses: actions/checkout@v3 93 | 94 | - name: Set up Python 3.9 95 | uses: actions/setup-python@v3 96 | with: 97 | python-version: 3.9 98 | cache: pip 99 | 100 | - name: Force Fail 101 | run: | 102 | exit 1 103 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | .idea/ 153 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.9" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | formats: 20 | - pdf 21 | - epub 22 | 23 | # Optionally declare the Python requirements required to build your docs 24 | python: 25 | install: 26 | - method: pip 27 | path: . 28 | extra_requirements: 29 | - docs 30 | 31 | - method: pip 32 | path: . 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.11.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.10.1...v1.11.0) (2023-08-20) 2 | 3 | 4 | ### Features 5 | 6 | * add email and password change functionality ([2fb370f](https://github.com/AsifArmanRahman/firebase-rest-api/commit/2fb370f3e1d134c6aa0c10428c76fc4bd9d26128)), closes [#20](https://github.com/AsifArmanRahman/firebase-rest-api/issues/20) 7 | 8 | 9 | 10 | ## [1.10.1](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.10.0...v1.10.1) (2023-04-07) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * unsupported error for empty string and dict ([bb84d9e](https://github.com/AsifArmanRahman/firebase-rest-api/commit/bb84d9e854fd8ca69b080666f1f76ae05e9b80a0)), closes [#11](https://github.com/AsifArmanRahman/firebase-rest-api/issues/11) 16 | 17 | 18 | 19 | # [1.10.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.9.0...v1.10.0) (2023-03-24) 20 | 21 | 22 | ### Features 23 | 24 | * **auth:** verify id token ([d896ab3](https://github.com/AsifArmanRahman/firebase-rest-api/commit/d896ab33a7347e5c99ee29b43395aec06e7dcab0)), closes [#9](https://github.com/AsifArmanRahman/firebase-rest-api/issues/9) 25 | 26 | 27 | 28 | # [1.9.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.8.1...v1.9.0) (2023-03-24) 29 | 30 | 31 | ### Features 32 | 33 | * set custom user claims ([9a5c9b9](https://github.com/AsifArmanRahman/firebase-rest-api/commit/9a5c9b958a3663178a49c5ed33cd0e4bbef4444c)), closes [#8](https://github.com/AsifArmanRahman/firebase-rest-api/issues/8) 34 | 35 | 36 | 37 | ## [1.8.1](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.8.0...v1.8.1) (2023-02-08) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * unsupported type error thrown for empty array field ([63cce77](https://github.com/AsifArmanRahman/firebase-rest-api/commit/63cce77420a999f0e151a32c8e389593b84dc357)), closes [#7](https://github.com/AsifArmanRahman/firebase-rest-api/issues/7) 43 | 44 | 45 | 46 | # [1.8.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.7.0...v1.8.0) (2022-08-15) 47 | 48 | 49 | ### Features 50 | 51 | * **auth:** sign in with facebook ([0bcead3](https://github.com/AsifArmanRahman/firebase-rest-api/commit/0bcead336128195932120c371be70f0afd5595ae)) 52 | * **firestore:** add list_of_documents() method ([04c8e20](https://github.com/AsifArmanRahman/firebase-rest-api/commit/04c8e20b98693e4e285266a571f7fcd9b7c10c4e)) 53 | 54 | 55 | 56 | # [1.7.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.6.0...v1.7.0) (2022-08-14) 57 | 58 | 59 | ### Features 60 | 61 | * **firestore:** support for complex queries ([6951e69](https://github.com/AsifArmanRahman/firebase-rest-api/commit/6951e6917ea31271da370e43cb5d8f0caa6d7d1f)) 62 | 63 | 64 | 65 | # [1.6.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.5.0...v1.6.0) (2022-08-13) 66 | 67 | 68 | ### Features 69 | 70 | * **firestore:** add add() method ([ec710ab](https://github.com/AsifArmanRahman/firebase-rest-api/commit/ec710ab5cb050f3799eb66430998bfb001f2e343)) 71 | 72 | 73 | 74 | # [1.5.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.4.0...v1.5.0) (2022-08-11) 75 | 76 | 77 | ### Features 78 | 79 | * **firestore:** add get method for collection ([7d9a193](https://github.com/AsifArmanRahman/firebase-rest-api/commit/7d9a19379914235be423bf36e208031fac28f48b)) 80 | 81 | 82 | 83 | # [1.4.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.0.1...v1.4.0) (2022-08-08) 84 | 85 | 86 | ### Features 87 | 88 | * **firestore:** add delete() method ([ba00cc2](https://github.com/AsifArmanRahman/firebase-rest-api/commit/ba00cc2ab9f1c29cc5e1306d39b5efe6b28af20f)) 89 | * **firestore:** add get() method ([dba57ea](https://github.com/AsifArmanRahman/firebase-rest-api/commit/dba57eab5dd1de1c5af22184cc6dead29fcb6d84)) 90 | * **firestore:** add set() method ([c8198f2](https://github.com/AsifArmanRahman/firebase-rest-api/commit/c8198f2fc6bc4605a3a50bf23c7ac823acc59cd2)) 91 | * **firestore:** add update() method ([e85c459](https://github.com/AsifArmanRahman/firebase-rest-api/commit/e85c459030441c413728e03a0646997e0fdc1a71)) 92 | 93 | 94 | ### Reverts 95 | 96 | * Revert "release: v1.0.2" ([4ce29e9](https://github.com/AsifArmanRahman/firebase-rest-api/commit/4ce29e92e3de0bdb1170ffa7ab2afdaf2bf16141)) 97 | * Revert "release: v1.1.0" ([55d3619](https://github.com/AsifArmanRahman/firebase-rest-api/commit/55d361920613bba9f2723784c39d6b3ce63c1ad1)) 98 | 99 | 100 | 101 | ## [1.0.1](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v1.0.0...v1.0.1) (2022-07-30) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * **auth:** create_custom_token ([240ef7c](https://github.com/AsifArmanRahman/firebase-rest-api/commit/240ef7cd61119c52ea4c78271fa5e9201c1da618)) 107 | 108 | 109 | 110 | # [1.0.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.7.0...v1.0.0) (2022-07-28) 111 | 112 | 113 | * chore!: update dependencies ([2551fdd](https://github.com/AsifArmanRahman/firebase-rest-api/commit/2551fdd64ef1d1411d689f9d61e01588d6620312)) 114 | 115 | 116 | ### BREAKING CHANGES 117 | 118 | * dependency changed from deprecated libraries 𝐨𝐚𝐮𝐭𝐡𝟐𝐜𝐥𝐢𝐞𝐧𝐭 to 𝐠𝐨𝐨𝐠𝐥𝐞-𝐚𝐮𝐭𝐡 and 𝐠𝐜𝐥𝐨𝐮𝐝 to 𝐠𝐨𝐨𝐠𝐥𝐞-𝐜𝐥𝐨𝐮𝐝-𝐬𝐭𝐨𝐫𝐚𝐠𝐞 119 | 120 | 121 | 122 | # [0.7.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.6.0...v0.7.0) (2022-07-24) 123 | 124 | 125 | ### Features 126 | 127 | * **auth:** add _𝘵𝘰𝘬𝘦𝘯_𝘧𝘳𝘰𝘮_𝘢𝘶𝘵𝘩_𝘶𝘳𝘭 method ([e444135](https://github.com/AsifArmanRahman/firebase-rest-api/commit/e444135a9d1107383405717029ccd0aab82f1f70)) 128 | * **auth:** Sign In with Google ([be6cb05](https://github.com/AsifArmanRahman/firebase-rest-api/commit/be6cb0551fc271cb3ae818de6f06137bbf44139d)) 129 | 130 | 131 | 132 | # [0.6.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.5.0...v0.6.0) (2022-07-24) 133 | 134 | 135 | ### Features 136 | 137 | * **auth:** add 𝘴𝘪𝘨𝘯_𝘪𝘯_𝘸𝘪𝘵𝘩_𝘰𝘢𝘶𝘵𝘩_𝘤𝘳𝘦𝘥𝘦𝘯𝘵𝘪𝘢𝘭 ([ad211a5](https://github.com/AsifArmanRahman/firebase-rest-api/commit/ad211a5bc3c9deddbe8441ff524d0008e0eb19a7)) 138 | * **auth:** client secret from file ([e5ea84e](https://github.com/AsifArmanRahman/firebase-rest-api/commit/e5ea84ed1a54246fe5a0709b7eafaaf7dd0aeb2c)) 139 | 140 | 141 | 142 | # [0.5.0](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.4.9...v0.5.0) (2022-07-23) 143 | 144 | 145 | ### Features 146 | 147 | * **auth:** add 𝘤𝘳𝘦𝘢𝘵𝘦_𝘢𝘶𝘵𝘩𝘦𝘯𝘵𝘪𝘤𝘢𝘵𝘪𝘰𝘯_𝘶𝘳𝘪 method ([b45a7f2](https://github.com/AsifArmanRahman/firebase-rest-api/commit/b45a7f203b0e4369bd501831ffca9f26c3eac464)) 148 | 149 | 150 | 151 | ## [0.4.9](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.4.8...v0.4.9) (2022-07-22) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * **storage:** proper link generate with 𝘨𝘦𝘵_𝘶𝘳𝘭 ([c825c16](https://github.com/AsifArmanRahman/firebase-rest-api/commit/c825c1695f4a0e87d4daa467c2ca654a9cc05248)) 157 | 158 | 159 | ### Performance Improvements 160 | 161 | * **storage:** use 𝘳𝘦𝘲𝘶𝘦𝘴𝘵 param ([f51d0dc](https://github.com/AsifArmanRahman/firebase-rest-api/commit/f51d0dc52e030bfe867d70c7728c3ccc32dc4334)) 162 | 163 | 164 | 165 | ## [0.4.8](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.4.7...v0.4.8) (2022-07-21) 166 | 167 | 168 | ### Bug Fixes 169 | 170 | * **storage:** set path via 𝘤𝘩𝘪𝘭𝘥 for 𝘥𝘦𝘭𝘦𝘵𝘦 ([b0527f0](https://github.com/AsifArmanRahman/firebase-rest-api/commit/b0527f0d418ad203df5845e1fd123bafe88a4b5d)) 171 | 172 | 173 | 174 | ## [0.4.7](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.4.6...v0.4.7) (2022-07-21) 175 | 176 | 177 | ### Bug Fixes 178 | 179 | * **storage:** set path via 𝘤𝘩𝘪𝘭𝘥 for 𝘥𝘰𝘸𝘯𝘭𝘰𝘢𝘥 ([da4d1de](https://github.com/AsifArmanRahman/firebase-rest-api/commit/da4d1deb9cfcc3c962f0240b70f9fee284dcd3e6)) 180 | 181 | 182 | 183 | ## [0.4.6](https://github.com/AsifArmanRahman/firebase-rest-api/compare/v0.4.5...v0.4.6) (2022-07-20) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * custom token generator ( [#1](https://github.com/AsifArmanRahman/firebase-rest-api/issues/1) ) ([b4cb169](https://github.com/AsifArmanRahman/firebase-rest-api/commit/b4cb1699d2d48d9741311d04a8530bf0242811e2)) 189 | 190 | 191 | 192 | ## [0.4.5](https://github.com/AsifArmanRahman/firebase-rest-api/compare/d0837260dcbc5ed4b890f38ac36b5dfa10d05e48...v0.4.5) (2022-07-19) 193 | 194 | 195 | ### Bug Fixes 196 | 197 | * **Build:** update requests version ([d083726](https://github.com/AsifArmanRahman/firebase-rest-api/commit/d0837260dcbc5ed4b890f38ac36b5dfa10d05e48)) 198 | 199 | 200 | ### Performance Improvements 201 | 202 | * use 𝘳𝘦𝘲𝘶𝘦𝘴𝘵 param ([db224f1](https://github.com/AsifArmanRahman/firebase-rest-api/commit/db224f1b75d57f77a8b118e6ed52ac22313e4fbf)) 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Asif Arman Rahman 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 | <div align="center"> 2 | 3 | <h1> Firebase REST API </h1> 4 | 5 | <p>A simple python wrapper for <a href="https://firebase.google.com">Google's Firebase REST API's</a>.</p> 6 | <br> 7 | 8 | </div> 9 | 10 | <div align="center"> 11 | <a href="https://pepy.tech/project/firebase-rest-api"> 12 | <img alt="Total Downloads" src="https://static.pepy.tech/personalized-badge/firebase-rest-api?period=total&units=international_system&left_color=blue&right_color=grey&left_text=Downloads"> 13 | </a> 14 | </div> 15 | 16 | <div align="center"> 17 | 18 | <a href="https://github.com/AsifArmanRahman/firebase-rest-api/actions/workflows/build.yml"> 19 | <img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/AsifArmanRahman/firebase-rest-api/build.yml?logo=GitHub"> 20 | </a> 21 | <a href="https://github.com/AsifArmanRahman/firebase-rest-api/actions/workflows/tests.yml"> 22 | <img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/asifarmanrahman/firebase-rest-api/tests.yml?label=tests&logo=Pytest"> 23 | </a> 24 | <a href="https://firebase-rest-api.readthedocs.io/en/latest/"> 25 | <img alt="Read the Docs" src="https://img.shields.io/readthedocs/firebase-rest-api?logo=Read%20the%20Docs&logoColor=white"> 26 | </a> 27 | <a href="https://codecov.io/gh/AsifArmanRahman/firebase-rest-api"> 28 | <img alt="CodeCov" src="https://codecov.io/gh/AsifArmanRahman/firebase-rest-api/branch/main/graph/badge.svg?token=N7TE1WVZ7W"> 29 | </a> 30 | 31 | </div> 32 | 33 | <div align="center"> 34 | <a href="https://pypi.org/project/firebase-rest-api/"> 35 | <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/firebase-rest-api?logo=python"> 36 | </a> 37 | <a href="https://pypi.org/project/firebase-rest-api/"> 38 | <img alt="PyPI" src="https://img.shields.io/pypi/v/firebase-rest-api?logo=PyPI&logoColor=white"> 39 | </a> 40 | </div> 41 | 42 | 43 | 44 | ## Installation 45 | 46 | ```python 47 | pip install firebase-rest-api 48 | ``` 49 | 50 | 51 | ## Quick Start 52 | 53 | In order to use this library, you first need to go through the following steps: 54 | 55 | 1. Select or create a Firebase project from [Firebase](https://console.firebase.google.com) Console. 56 | 57 | 2. Register an Web App. 58 | 59 | 60 | ### Example Usage 61 | 62 | ```python 63 | # Import Firebase REST API library 64 | import firebase 65 | 66 | # Firebase configuration 67 | config = { 68 | "apiKey": "apiKey", 69 | "authDomain": "projectId.firebaseapp.com", 70 | "databaseURL": "https://databaseName.firebaseio.com", 71 | "projectId": "projectId", 72 | "storageBucket": "projectId.appspot.com", 73 | "messagingSenderId": "messagingSenderId", 74 | "appId": "appId" 75 | } 76 | 77 | # Instantiates a Firebase app 78 | app = firebase.initialize_app(config) 79 | 80 | 81 | # Firebase Authentication 82 | auth = app.auth() 83 | 84 | # Create new user and sign in 85 | auth.create_user_with_email_and_password(email, password) 86 | user = auth.sign_in_with_email_and_password(email, password) 87 | 88 | 89 | # Firebase Realtime Database 90 | db = app.database() 91 | 92 | # Data to save in database 93 | data = { 94 | "name": "Robert Downey Jr.", 95 | "email": user.get('email') 96 | } 97 | 98 | # Store data to Firebase Database 99 | db.child("users").push(data, user.get('idToken')) 100 | 101 | 102 | # Firebase Storage 103 | storage = app.storage() 104 | 105 | # File to store in storage 106 | file_path = 'static/img/example.png' 107 | 108 | # Store file to Firebase Storage 109 | storage.child(user.get('localId')).child('uploaded-picture.png').put(file_path, user.get('idToken')) 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/_static/css/my-styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-top: 40px; 3 | margin-bottom: 30px; 4 | padding-bottom: 20px; 5 | border-bottom: 2px solid black; 6 | width: 100%; 7 | } 8 | 9 | h2 { 10 | margin-top: 30px; 11 | margin-bottom: 20px; 12 | padding-bottom: 15px; 13 | border-bottom: 1px solid black; 14 | width: 75%; 15 | } 16 | 17 | h3 { 18 | margin-top: 15px; 19 | margin-bottom: 10px; 20 | padding-bottom: 0; 21 | } 22 | 23 | .highlight { 24 | background: #e5f5f5; 25 | margin-top: 15pt; 26 | } -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import toml 16 | 17 | sys.path.insert(0, os.path.abspath('..')) 18 | 19 | project_config = toml.load('..//pyproject.toml') 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'firebase-rest-api' 24 | copyright = '2022, Asif Arman Rahman' 25 | author = 'Asif Arman Rahman' 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = project_config['project']['version'] 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.autosectionlabel', 39 | 'sphinx.ext.intersphinx', 40 | 'sphinx.ext.napoleon', 41 | 'sphinx.ext.viewcode', 42 | 'sphinx_design', 43 | ] 44 | 45 | # auto section label configuration 46 | autosectionlabel_prefix_document = True 47 | autosectionlabel_maxdepth = 3 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # List of patterns, relative to source directory, that match files and 53 | # directories to ignore when looking for source files. 54 | # This pattern also affects html_static_path and html_extra_path. 55 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 56 | 57 | # Example configuration for intersphinx: refer to the Python standard library. 58 | intersphinx_mapping = { 59 | 'google': ("https://googleapis.dev/python/google-auth/latest/", None), 60 | 'google.cloud': ("https://googleapis.dev/python/storage/latest/", None), 61 | 'google.cloud.firestore': ("https://googleapis.dev/python/firestore/latest/", None), 62 | 'python': ("https://docs.python.org/3/", None), 63 | 'requests': ("https://requests.readthedocs.io/en/stable/", None), 64 | } 65 | 66 | 67 | # -- Options for HTML output ------------------------------------------------- 68 | 69 | # The theme to use for HTML and HTML Help pages. See the documentation for 70 | # a list of builtin themes. 71 | html_theme = 'sphinx_rtd_theme' 72 | 73 | # Add any paths that contain custom static files (such as style sheets) here, 74 | # relative to this directory. They are copied after the builtin static files, 75 | # so a file named "default.css" will overwrite the builtin "default.css". 76 | html_static_path = ['_static'] 77 | 78 | # custom static css files to add 79 | html_css_files = [ 80 | "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/fontawesome.min.css", 81 | "css/my-styles.css", 82 | ] 83 | -------------------------------------------------------------------------------- /docs/firebase/firebase.auth.rst: -------------------------------------------------------------------------------- 1 | firebase.auth package 2 | --------------------- 3 | 4 | .. automodule:: firebase.auth 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/firebase/firebase.database.rst: -------------------------------------------------------------------------------- 1 | firebase.database package 2 | ========================= 3 | 4 | .. automodule:: firebase.database 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/firebase/firebase.firestore.rst: -------------------------------------------------------------------------------- 1 | firebase.firestore package 2 | -------------------------- 3 | 4 | .. automodule:: firebase.firestore 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/firebase/firebase.rst: -------------------------------------------------------------------------------- 1 | firebase 2 | -------- 3 | 4 | .. automodule:: firebase 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/firebase/firebase.storage.rst: -------------------------------------------------------------------------------- 1 | firebase.storage package 2 | ======================== 3 | 4 | .. automodule:: firebase.storage 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/firebase/modules.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | firebase 8 | firebase.auth 9 | firebase.database 10 | firebase.firestore 11 | firebase.storage 12 | -------------------------------------------------------------------------------- /docs/guide/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | The authentication service allows you to signup, login, 5 | edit profile, apply security to the data you might store 6 | in either :ref:`Database<guide/database:Database>` or 7 | :ref:`Storage<guide/storage:Storage>`, and of course delete 8 | your account. 9 | 10 | .. code-block:: python 11 | 12 | # Get a reference to the auth service 13 | auth = firebaseApp.auth() 14 | .. 15 | 16 | .. note:: 17 | All sign in methods return user data, including a token 18 | you can use to adhere the security rules. 19 | 20 | 21 | create_user_with_email_and_password 22 | ----------------------------------- 23 | 24 | Users can create an account using their 25 | email address and choice of password. 26 | 27 | .. code-block:: python 28 | 29 | # Creating an account 30 | auth.create_user_with_email_and_password(email, password) 31 | .. 32 | 33 | .. note:: 34 | Make sure you have the Email/Password provider enabled in your 35 | Firebase dashboard under Authentication -> Sign In Method. 36 | 37 | 38 | sign_in_with_email_and_password 39 | ------------------------------- 40 | 41 | User can login using their email and password, provided they 42 | :ref:`created an account<guide/authentication:create_user_with_email_and_password>` 43 | first. 44 | 45 | .. code-block:: python 46 | 47 | # Log the user in 48 | user = auth.sign_in_with_email_and_password(email, password) 49 | .. 50 | 51 | 52 | create_custom_token 53 | ------------------- 54 | 55 | | You can also create users using `custom tokens`_, 56 | | For example: 57 | 58 | .. code-block:: python 59 | 60 | # Create custom token 61 | token = auth.create_custom_token("your_custom_id") 62 | .. 63 | 64 | You can also pass in additional claims. 65 | 66 | .. code-block:: python 67 | 68 | # Create custom token with claims 69 | token_with_additional_claims = auth.create_custom_token("your_custom_id", {"premium_account": True}) 70 | .. 71 | 72 | .. note:: 73 | You need admin credentials (Service Account Key) to create 74 | custom tokens. 75 | 76 | .. _custom tokens: 77 | https://firebase.google.com/docs/auth/server/create-custom-tokens 78 | 79 | 80 | sign_in_with_custom_token 81 | ------------------------- 82 | 83 | You can send these custom tokens to the client to 84 | sign in, or sign in as the user on the server. 85 | 86 | .. code-block:: python 87 | 88 | # log in user using custom token 89 | user = auth.sign_in_with_custom_token(token) 90 | .. 91 | 92 | 93 | set_custom_user_claims 94 | ---------------------- 95 | 96 | You can add custom claims to existing user, or remove 97 | claims which was previously added to that account. 98 | 99 | .. code-block:: python 100 | 101 | # add claims 102 | auth.set_custom_user_claims(user['localId'], {'premium': True}) 103 | 104 | # remove claims 105 | auth.set_custom_user_claims(user['localId'], {'premium': None}) 106 | .. 107 | 108 | .. note:: 109 | 1. You need admin credentials (Service Account Key) to add or 110 | remove custom claims. 111 | 112 | 2. The new custom claims will propagate to the user's ID token 113 | the next time a new token is issued. 114 | 115 | 116 | verify_id_token 117 | --------------- 118 | 119 | You can decode the Firebase ID token, and check for claims. 120 | 121 | .. code-block:: python 122 | 123 | # check if user is subscribed to premium 124 | claims = auth.verify_id_token(user['IdToken']) 125 | 126 | if claims['premium'] is True: 127 | # Allow access to requested premium resource. 128 | pass 129 | .. 130 | 131 | 132 | sign_in_anonymous 133 | ----------------- 134 | 135 | Allows users (who haven't signed up yet) to 136 | use your app without creating an account. 137 | 138 | 139 | .. code-block:: python 140 | 141 | # Log the user in anonymously 142 | user = auth.sign_in_anonymous() 143 | .. 144 | 145 | .. note:: 146 | Make sure you have the **Anonymous** provider enabled in your 147 | Firebase dashboard under Authentication -> Sign In Method. 148 | 149 | 150 | create_authentication_uri 151 | ------------------------- 152 | 153 | Signing in with social providers is done through two steps. First step 154 | one is done via redirecting user to the providers' login page using 155 | :ref:`create_authentication_uri<guide/authentication:create_authentication_uri>` 156 | which is can be used dynamically for all providers. 157 | 158 | 159 | .. warning:: 160 | At the moment only sign is via **Google** is supported, other 161 | ones might break or work. 162 | 163 | The method returns an link to redirect user to providers' sign in page. 164 | Once the user signs into their account, user is asked for permissions 165 | and when granted, are redirect to the uri set while creating 166 | **OAuth Client IDs**, with authorization code to which can be further 167 | used to generate tokens to sign in with social providers in 168 | :ref:`second step<guide/authentication:sign_in_with_oauth_credential>`. 169 | 170 | .. code-block:: python 171 | 172 | # Get a reference to the auth service with provider secret file 173 | auth = firebaseApp.auth(client_secret='client-secret-file.json') 174 | 175 | # Reference to auth service with provider secret from env variable 176 | client_secret_config = { 177 | "client_id": environ.get("CLIENT_ID"), 178 | "client_secret": environ.get("CLIENT_SECRET"), 179 | "redirect_uris": [environ.get("REDIRECT_URI")] 180 | } 181 | 182 | auth = firebaseApp.auth(client_secret=client_secret_config) 183 | .. 184 | 185 | .. code-block:: python 186 | 187 | # Example usage with Flask 188 | @auth.route('/login/google') 189 | def login_google(): 190 | return redirect(auth.create_authentication_uri('google.com')) 191 | 192 | .. 193 | 194 | .. note:: 195 | Make sure you have the **social** provider enabled in your 196 | Firebase dashboard under Authentication -> Sign In Method. 197 | 198 | 199 | authenticate_login_with_google 200 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 201 | 202 | This method is actually an reference to 203 | :ref:`create_authentication_uri<guide/authentication:create_authentication_uri>` 204 | with **Google** preset as the provider to use. 205 | 206 | 207 | .. code-block:: python 208 | 209 | # Example usage with Flask 210 | @auth.route('/login/google') 211 | def login_google(): 212 | return redirect(auth.authenticate_login_with_google()) 213 | .. 214 | 215 | .. note:: 216 | Make sure you have the **Google Sign In** provider enabled in 217 | your Firebase dashboard under Authentication -> Sign In Method. 218 | 219 | 220 | authenticate_login_with_facebook 221 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 222 | 223 | This method is actually an reference to 224 | :ref:`create_authentication_uri<guide/authentication:create_authentication_uri>` 225 | with **Facebook** preset as the provider to use. 226 | 227 | 228 | .. code-block:: python 229 | 230 | # Example usage with Flask 231 | @auth.route('/login/facebook') 232 | def login_facebook(): 233 | return redirect(auth.authenticate_login_with_facebook()) 234 | .. 235 | 236 | .. note:: 237 | Make sure you have the **Google Sign In** provider enabled in 238 | your Firebase dashboard under Authentication -> Sign In Method. 239 | 240 | 241 | 242 | sign_in_with_oauth_credential 243 | ----------------------------- 244 | 245 | Second step to sign in using social provider is to pass the URL 246 | (containing multiple params) that the user is redirected to, into this 247 | method. This method auto generates the tokens using params from that 248 | URL, then signs the user in using those tokens to Firebase linking the 249 | specific provider. 250 | 251 | 252 | .. code-block:: python 253 | 254 | # Here https://example.com/oauth2callback/ is the redirect URI 255 | # that was set while creating OAuth Client ID 256 | 257 | # Example usage with Flask 258 | @auth.route('/oauth2callback/') 259 | def oauth2callback(): 260 | 261 | user = auth.sign_in_with_oauth_credential(request.url) 262 | 263 | return jsonify(**user) 264 | 265 | 266 | get_account_info 267 | ---------------- 268 | 269 | This method returns an detailed version of the user's data associated 270 | with Authentication service. 271 | 272 | .. code-block:: python 273 | 274 | # User account info 275 | user_info = auth.get_account_info(user['idToken']) 276 | .. 277 | 278 | 279 | update_profile 280 | -------------- 281 | 282 | Update stored information or add information into the user's account. 283 | 284 | .. code-block:: python 285 | 286 | # Update user's name 287 | auth.update_profile(user['idToken'], display_name='Iron Man') 288 | 289 | # update user's profile picture 290 | auth.update_profile(user['idToken'], photo_url='https://i.pinimg.com/originals/c0/37/2f/c0372feb0069e6289eb938b219e0b0a1.jpg') 291 | .. 292 | 293 | 294 | change_email 295 | -------------- 296 | 297 | Change the email associated with the user's account. 298 | 299 | .. code-block:: python 300 | 301 | # change user's email 302 | auth.change_email(user['idToken'], email='iam@ironman.com') 303 | 304 | .. 305 | 306 | 307 | change_password 308 | -------------- 309 | 310 | Change the password associated with the user's account. 311 | 312 | .. code-block:: python 313 | 314 | # change user's password 315 | auth.change_password(user['idToken'], password='iLoveYou3000') 316 | 317 | .. 318 | 319 | 320 | refresh 321 | ------- 322 | 323 | Firebase Auth Tokens are granted when an user logs in, and are 324 | associated with an expiration time of an hour generally, after 325 | that they lose validation and a new set of Tokens are needed, 326 | and they can be obtained by passing the ``refreshToken`` key 327 | from the users' tokens, received when logged in. 328 | 329 | .. code-block:: python 330 | 331 | # before the 1 hour expiry: 332 | user = auth.refresh(user['refreshToken']) 333 | 334 | # now we have a fresh token 335 | user['idToken'] 336 | .. 337 | 338 | 339 | delete_user_account 340 | ------------------- 341 | 342 | In case any user want to delete their account, it can be done by 343 | passing ``idToken`` key from the users' tokens, received when logged 344 | in. 345 | 346 | .. code-block:: python 347 | 348 | auth.delete_user_account(user['idToken']) 349 | .. 350 | 351 | 352 | send_password_reset_email 353 | ------------------------- 354 | 355 | In case any user forgot his password, it is possible to send 356 | them email containing an code or link to reset their password. 357 | 358 | .. code-block:: python 359 | 360 | auth.send_password_reset_email(email) 361 | .. 362 | 363 | 364 | send_email_verification 365 | ----------------------- 366 | 367 | To ensure the email address belongs to the user who created the 368 | account, it is recommended to request verification of the email. 369 | Verification code/link can be sent to the user by passing ``idToken`` 370 | key from the users' tokens, to this method. 371 | 372 | .. code-block:: python 373 | 374 | auth.send_email_verification(user['idToken']) 375 | .. 376 | -------------------------------------------------------------------------------- /docs/guide/database.rst: -------------------------------------------------------------------------------- 1 | Database 2 | ======== 3 | 4 | The database service allows you to run CRUD operations to your Firebase Realtime 5 | Database, and also perform complex queries while doing so. 6 | 7 | .. code-block:: python 8 | 9 | # Create database instance 10 | db = firebaseApp.database() 11 | .. 12 | 13 | .. note:: 14 | Each of the following methods accepts a user token: 15 | :ref:`get()<guide/database:get>`, :ref:`push()<guide/database:push>`, 16 | :ref:`set()<guide/database:set>`, :ref:`update()<guide/database:update>`, 17 | :ref:`remove()<guide/database:remove>` and 18 | :ref:`stream()<guide/database:streaming>`. 19 | 20 | 21 | Build Path 22 | ---------- 23 | 24 | You can build paths to your data by using the ``child()`` method. 25 | 26 | .. code-block:: python 27 | 28 | db.child("users").child("Edward") 29 | 30 | # Alternate ways 31 | db.child("users", "Edward") 32 | db.child("users/Edward") 33 | .. 34 | 35 | 36 | Save Data 37 | --------- 38 | 39 | 40 | push 41 | ^^^^ 42 | 43 | To save data with a unique, auto-generated, timestamp-based key, use the 44 | ``push()`` method. 45 | 46 | .. code-block:: python 47 | 48 | data = {"name": "Anthony 'Edward' Stark"} 49 | db.child("users").push(data) 50 | .. 51 | 52 | set 53 | ^^^ 54 | 55 | To create your own keys use the ``set()`` method. The key in the example 56 | below is "Morty". 57 | 58 | .. code-block:: python 59 | 60 | data = {"name": "Anthony 'Edward' Stark"} 61 | db.child("users").child("Edward").set(data) 62 | .. 63 | 64 | update 65 | ^^^^^^ 66 | 67 | To update data for an existing entry use the ``update()`` method. 68 | 69 | .. code-block:: python 70 | 71 | db.child("users").child("Edward").update({"name": "Tony Stark"}) 72 | .. 73 | 74 | remove 75 | ^^^^^^ 76 | 77 | To delete data for an existing entry use the ``remove()`` method. 78 | 79 | .. code-block:: python 80 | 81 | db.child("users").child("Edward").remove() 82 | .. 83 | 84 | multi-location updates 85 | ^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | You can also perform `multi-location 88 | updates <https://www.firebase.com/blog/2015-09-24-atomic-writes-and-more.html>`__ 89 | with the ``update()`` method. 90 | 91 | .. code-block:: python 92 | 93 | data = { 94 | "users/Edward/": { 95 | "name": "Anthony 'Edward' Stark" 96 | }, 97 | "users/Pepper/": { 98 | "name": "Virginia 'Pepper' Potts" 99 | } 100 | } 101 | 102 | db.update(data) 103 | .. 104 | 105 | To perform multi-location writes to new locations we can use the 106 | ``generate_key()`` method. 107 | 108 | .. code-block:: python 109 | 110 | data = { 111 | "users/"+ref.generate_key(): { 112 | "name": "Anthony 'Edward' Stark" 113 | }, 114 | "users/"+ref.generate_key(): { 115 | "name": "Virginia 'Pepper' Potts" 116 | } 117 | } 118 | 119 | db.update(data) 120 | .. 121 | 122 | 123 | Retrieve Data 124 | ------------- 125 | 126 | get 127 | ^^^ 128 | 129 | To return data from a path simply call the ``get()`` method. 130 | 131 | .. code-block:: python 132 | 133 | users = db.child("users").get() 134 | .. 135 | 136 | each 137 | ^^^^ 138 | 139 | Returns a list of objects on each of which you can call ``val()`` and 140 | ``key()``. 141 | 142 | .. code-block:: python 143 | 144 | users = db.child("users").get() 145 | for user in users.each(): 146 | print(user.key(), user.val()) 147 | 148 | # Output: 149 | # Edward {name": "Anthony 'Edward' Stark"} 150 | # Pepper {'name': "Virginia 'Pepper' Potts"} 151 | .. 152 | 153 | 154 | val 155 | ^^^ 156 | 157 | Queries return a PyreResponse object. Calling ``val()`` on these objects 158 | returns the query data. 159 | 160 | .. code-block:: python 161 | 162 | users = db.child('users').child('Edward').get() 163 | 164 | for user in users.each(): 165 | print(user.val()) 166 | 167 | # Output: 168 | # {'name': "Anthony 'Edward' Stark"} 169 | .. 170 | 171 | key 172 | ^^^ 173 | 174 | Calling ``key()`` returns the key for the query data. 175 | 176 | .. code-block:: python 177 | 178 | users = db.child("users").get() 179 | 180 | for user in users.each(): 181 | print(user.key()) 182 | 183 | # Output: 184 | # Edward 185 | # Pepper 186 | .. 187 | 188 | 189 | Conditional Requests 190 | ^^^^^^^^^^^^^^^^^^^^ 191 | 192 | It's possible to do conditional sets and removes by using the 193 | ``conditional_set()`` and ``conitional_remove()`` methods respectively. 194 | You can read more about conditional requests in Firebase 195 | `here <https://firebase.google.com/docs/reference/rest/database/#section-conditional-requests>`__. 196 | 197 | To use these methods, you first get the ETag of a particular path by 198 | using the ``get_etag()`` method. You can then use that tag in your 199 | conditional request. 200 | 201 | .. code-block:: python 202 | 203 | etag = db.child("users").child("Edward").get_etag() 204 | data = {"name": "Tony Stark"} 205 | db.child("users").child("Edward").conditional_set(data, etag) 206 | .. 207 | 208 | If the passed ETag does not match the ETag of the path in the database, 209 | the data will not be written, and both conditional request methods will 210 | return a single key-value pair with the new ETag to use of the following 211 | form: 212 | 213 | .. code-block:: json 214 | 215 | { "ETag": "8KnE63B6HiKp67Wf3HQrXanujSM=" } 216 | .. 217 | 218 | Here's an example of checking whether or not a conditional removal was 219 | successful: 220 | 221 | .. code-block:: python 222 | 223 | etag = db.child("users").child("Edward").get_etag() 224 | response = db.child("users").child("Edward").conditional_remove(etag) 225 | 226 | if "ETag" in response: 227 | etag = response["ETag"] # our ETag was out-of-date 228 | else: 229 | print("We removed the data successfully!") 230 | .. 231 | 232 | shallow 233 | ^^^^^^^ 234 | 235 | To return just the keys at a particular path use the ``shallow()`` 236 | method. 237 | 238 | .. code-block:: python 239 | 240 | all_user_ids = db.child("users").shallow().get() 241 | .. 242 | 243 | .. note:: 244 | ``shallow()`` can not be used in conjunction with any complex 245 | queries. 246 | 247 | streaming 248 | ^^^^^^^^^ 249 | 250 | You can listen to live changes to your data with the ``stream()`` 251 | method. 252 | 253 | .. code-block:: python 254 | 255 | def stream_handler(message): 256 | print(message["event"]) # put 257 | print(message["path"]) # /-K7yGTTEp7O549EzTYtI 258 | print(message["data"]) # {'title': 'Firebase', "body": "etc..."} 259 | 260 | my_stream = db.child("posts").stream(stream_handler) 261 | .. 262 | 263 | You should at least handle ``put`` and ``patch`` events. Refer to 264 | `"Streaming from the REST 265 | API" <https://firebase.google.com/docs/reference/rest/database/#section-streaming>`__ 266 | for details. 267 | 268 | You can also add a ``stream_id`` to help you identify a stream if you 269 | have multiple running: 270 | 271 | .. code-block:: python 272 | 273 | my_stream = db.child("posts").stream(stream_handler, stream_id="new_posts") 274 | .. 275 | 276 | close the stream 277 | ^^^^^^^^^^^^^^^^ 278 | 279 | .. code-block:: python 280 | 281 | my_stream.close() 282 | .. 283 | 284 | 285 | Complex Queries 286 | --------------- 287 | 288 | Queries can be built by chaining multiple query parameters together. 289 | 290 | .. code-block:: python 291 | 292 | users_by_name = db.child("users").order_by_child("name").limit_to_first(3).get() 293 | .. 294 | 295 | This query will return the first three users ordered by name. 296 | 297 | order_by_child 298 | ^^^^^^^^^^^^^^ 299 | 300 | We begin any complex query with ``order_by_child()``. 301 | 302 | .. code-block:: python 303 | 304 | users_by_name = db.child("users").order_by_child("name").get() 305 | .. 306 | 307 | This query will return users ordered by name. 308 | 309 | equal_to 310 | ^^^^^^^^ 311 | 312 | Return data with a specific value. 313 | 314 | .. code-block:: python 315 | 316 | users_by_score = db.child("users").order_by_child("score").equal_to(10).get() 317 | .. 318 | 319 | This query will return users with a score of 10. 320 | 321 | start_at and end_at 322 | ^^^^^^^^^^^^^^^^^^^ 323 | 324 | Specify a range in your data. 325 | 326 | .. code-block:: python 327 | 328 | users_by_score = db.child("users").order_by_child("score").start_at(3).end_at(10).get() 329 | .. 330 | 331 | This query returns users ordered by score and with a score between 3 and 332 | 10. 333 | 334 | limit_to_first and limit_to_last 335 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 336 | 337 | Limits data returned. 338 | 339 | .. code-block:: python 340 | 341 | users_by_score = db.child("users").order_by_child("score").limit_to_first(5).get() 342 | .. 343 | 344 | This query returns the first five users ordered by score. 345 | 346 | order_by_key 347 | ^^^^^^^^^^^^ 348 | 349 | When using ``order_by_key()`` to sort your data, data is returned in 350 | ascending order by key. 351 | 352 | .. code-block:: python 353 | 354 | users_by_key = db.child("users").order_by_key().get() 355 | .. 356 | 357 | order_by_value 358 | ^^^^^^^^^^^^^^ 359 | 360 | When using ``order_by_value()``, children are ordered by their value. 361 | 362 | .. code-block:: python 363 | 364 | users_by_value = db.child("users").order_by_value().get() 365 | .. 366 | 367 | 368 | Helper Methods 369 | -------------- 370 | 371 | generate_key 372 | ^^^^^^^^^^^^ 373 | 374 | ``db.generate_key()`` is an implementation of Firebase's `key generation 375 | algorithm <https://www.firebase.com/blog/2015-02-11-firebase-unique-identifiers.html>`__. 376 | 377 | See :ref:`multi-location updates<guide/database:multi-location updates>` 378 | for a potential use case. 379 | 380 | 381 | sort 382 | ^^^^ 383 | 384 | Sometimes we might want to sort our data multiple times. For example, we 385 | might want to retrieve all articles written between a certain date then 386 | sort those articles based on the number of likes. 387 | 388 | Currently the REST API only allows us to sort our data once, so the 389 | ``sort()`` method bridges this gap. 390 | 391 | .. code-block:: python 392 | 393 | articles = db.child("articles").order_by_child("date").start_at(startDate).end_at(endDate).get() 394 | articles_by_likes = db.sort(articles, "likes") 395 | .. 396 | 397 | 398 | Common Errors 399 | ------------- 400 | 401 | Index not defined 402 | ^^^^^^^^^^^^^^^^^ 403 | 404 | + `Indexing`_ is **not enabled** for the database reference. 405 | 406 | .. _Indexing: https://firebase.google.com/docs/database/security/indexing-data 407 | -------------------------------------------------------------------------------- /docs/guide/firebase-rest-api.rst: -------------------------------------------------------------------------------- 1 | Integrate Firebase 2 | ################## 3 | 4 | You can integrate Firebase project into your Python app in 5 | two ways. 6 | 7 | User based Authentication 8 | ************************* 9 | 10 | For use with only user based authentication we can create the 11 | following configuration: 12 | 13 | .. code-block:: python 14 | 15 | # Import Firebase REST API library 16 | import firebase 17 | 18 | # Firebase configuration 19 | config = { 20 | "apiKey": "apiKey", 21 | "authDomain": "projectId.firebaseapp.com", 22 | "databaseURL": "https://databaseName.firebaseio.com", 23 | "projectId": "projectId", 24 | "storageBucket": "projectId.appspot.com", 25 | "messagingSenderId": "messagingSenderId", 26 | "appId": "appId" 27 | } 28 | 29 | # Instantiates a Firebase app 30 | firebaseApp = firebase.initialize_app(config) 31 | .. 32 | 33 | 34 | Admin based Authentication 35 | ************************** 36 | 37 | We can optionally send `service account credential`_ to our app that 38 | will allow our server to authenticate with Firebase as an **admin** 39 | and disregard any security rules. 40 | 41 | .. _service account credential: https://firebase.google.com/docs/server/setup#prerequisites 42 | 43 | 44 | Service Account Secret File 45 | =========================== 46 | 47 | The following example uses the service account secrets `file` path 48 | as the value for `serviceAccount` key. 49 | 50 | .. code-block:: python 51 | 52 | # Import Firebase REST API library 53 | import firebase 54 | 55 | # Firebase configuration with service account secret file path 56 | config = { 57 | "apiKey": "apiKey", 58 | "authDomain": "projectId.firebaseapp.com", 59 | "databaseURL": "https://databaseName.firebaseio.com", 60 | "projectId": "projectId", 61 | "storageBucket": "projectId.appspot.com", 62 | "messagingSenderId": "messagingSenderId", 63 | "appId": "appId" 64 | 65 | "serviceAccount": "path/to/serviceAccountCredentials.json" 66 | } 67 | 68 | firebaseApp = firebase.initialize_app(config) 69 | .. 70 | 71 | 72 | Service Account Secret Dict 73 | =========================== 74 | 75 | 76 | The following example uses the service account secrets `dict` 77 | as the value for `serviceAccount` key. 78 | 79 | .. code-block:: python 80 | 81 | # Import Firebase REST API library 82 | import firebase 83 | 84 | # Firebase configuration 85 | config = { 86 | "apiKey": "apiKey", 87 | "authDomain": "projectId.firebaseapp.com", 88 | "databaseURL": "https://databaseName.firebaseio.com", 89 | "projectId": "projectId", 90 | "storageBucket": "projectId.appspot.com", 91 | "messagingSenderId": "messagingSenderId", 92 | "appId": "appId" 93 | } 94 | 95 | # Service Account Secret dict 96 | service_account_key = { 97 | "type": "service_account", 98 | "project_id": "project_id", 99 | "private_key_id": "private_key_id", 100 | "private_key": "private_key", 101 | "client_email": "client_email", 102 | "client_id": "client_id", 103 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 104 | "token_uri": "https://oauth2.googleapis.com/token", 105 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 106 | "client_x509_cert_url": "client_x509_cert_url" 107 | } 108 | 109 | config['serviceAccount'] = service_account_key 110 | 111 | firebaseApp = firebase.initialize_app(config) 112 | .. 113 | 114 | .. note:: 115 | Adding a service account will authenticate as an admin 116 | by default for all database queries, check out the 117 | :ref:`Authentication documentation<guide/authentication:Authentication>` 118 | for how to authenticate users. 119 | 120 | Use Services 121 | ************ 122 | 123 | A Firebase app can use multiple Firebase services. 124 | 125 | ``firebaseApp.auth()`` - :ref:`Authentication<guide/authentication:Authentication>` 126 | 127 | ``firebaseApp.database()`` - :ref:`Database<guide/database:Database>` 128 | 129 | ``firebaseApp.firestore()`` - :ref:`Firestore<guide/firestore:Firestore>` 130 | 131 | ``firebaseApp.storage()`` - :ref:`Storage<guide/storage:Storage>` 132 | 133 | Check out the documentation for each service for further details. 134 | -------------------------------------------------------------------------------- /docs/guide/firestore.rst: -------------------------------------------------------------------------------- 1 | Firestore 2 | ========= 3 | 4 | The firestore service allows you to run CRUD operations to your Firebase Firestore 5 | Database. 6 | 7 | .. code-block:: python 8 | 9 | # Create database instance 10 | fsdb = firebaseApp.firestore() 11 | .. 12 | 13 | .. note:: 14 | Each of the following methods accepts a user token: 15 | ``get()``, ``set()``, ``update()``, and ``delete()``. 16 | 17 | 18 | Build Path 19 | ---------- 20 | 21 | You can build paths to your data by using the ``collection()`` and ``document()`` method. 22 | 23 | .. code-block:: python 24 | 25 | fsdb.collection('Marvels').document('Movies') 26 | fsdb.collection('Marvels').document('Movies').collection('PhaseOne').document('2008') 27 | 28 | .. 29 | 30 | .. note:: 31 | The methods available/used after ``collection()`` method and 32 | ``document()`` method are **NOT SAME**. Both method is a 33 | reference to different classes with different methods in them. 34 | 35 | 36 | Save Data 37 | --------- 38 | 39 | set 40 | ^^^ 41 | 42 | To store data in a collection named ``Marvels`` and a document inside 43 | the collection named ``Movies``, use ``set()`` method. 44 | 45 | .. code-block:: python 46 | 47 | data = { 48 | "name": "Iron Man", 49 | "lead": { 50 | "name": "Robert Downey Jr." 51 | }, 52 | 'cast': ['Gwyneth Paltrow'] 53 | 'released': False, 54 | 'prequel': None 55 | } 56 | 57 | fsdb.collection('Marvels').document('Movies').set(data) 58 | .. 59 | 60 | .. attention:: 61 | Using this method on an existing document will overwrite the existing 62 | document. 63 | 64 | 65 | add 66 | ^^^ 67 | 68 | To store data in a collection named ``Marvels`` within an auto 69 | generated document ID, use ``add()`` method. 70 | 71 | .. code-block:: python 72 | 73 | data = { 74 | "name": "Iron Man", 75 | "lead": { 76 | "name": "Robert Downey Jr." 77 | }, 78 | 'cast': ['Gwyneth Paltrow'] 79 | 'released': False, 80 | 'prequel': None 81 | } 82 | 83 | id = fsdb.collection('Marvels').add(data) 84 | .. 85 | 86 | 87 | Read Data 88 | --------- 89 | 90 | |document-get| 91 | ^^^^^^^^^^^^^^ 92 | 93 | .. |document-get| replace:: 94 | get 95 | 96 | To read data from an existing document ``Movies`` of the collection 97 | ``Marvels``, use ``get()`` method. 98 | 99 | .. code-block:: python 100 | 101 | fsdb.collection('Marvels').document('Movies').get() 102 | .. 103 | 104 | 105 | 106 | It is possible to filter the data of an document to receive specific fields. 107 | 108 | .. code-block:: python 109 | 110 | fsdb.collection('Marvels').document('Movies').get(field_paths=['lead.name', 'released']) 111 | 112 | # Output: 113 | # {'lead': {'name': "Robert Downey Jr."}, 'released': False} 114 | .. 115 | 116 | 117 | |collection-get| 118 | ^^^^^^^^^^^^^^^^ 119 | 120 | .. |collection-get| replace:: 121 | get 122 | 123 | To fetch data regarding all existing document (document ID and the data 124 | it contains) of an collection ``Marvels``, use ``get()`` method. 125 | 126 | .. code-block:: python 127 | 128 | fsdb.collection('Marvels').get() 129 | .. 130 | 131 | .. warning:: 132 | This ``get()`` method is different from the above stated one, and 133 | receives different parameters and returns different output. 134 | 135 | 136 | list_of_documents 137 | ^^^^^^^^^^^^^^^^^ 138 | 139 | To fetch all existing document ID's in a collection ``Marvels``, use 140 | ``list_of_documents()`` method. 141 | 142 | .. code-block:: python 143 | 144 | fsdb.collection('Marvels').list_of_documents() 145 | .. 146 | 147 | 148 | Update Data 149 | ----------- 150 | 151 | update 152 | ^^^^^^ 153 | 154 | To add more data to an existing document, use ``update()`` method. 155 | 156 | .. code-block:: python 157 | 158 | # add new data to an existing document 159 | 160 | data = { 161 | 'year': 2008, 162 | } 163 | 164 | fsdb.collection('Marvels').document('Movies').update(data) 165 | .. 166 | 167 | 168 | 169 | To update existing data to an existing document, use ``update()`` method. 170 | 171 | .. code-block:: python 172 | 173 | # update data of an existing document 174 | 175 | data = { 176 | 'released': True, 177 | } 178 | 179 | fsdb.collection('Marvels').document('Movies').update(data) 180 | .. 181 | 182 | 183 | 184 | To add an item to an array field in an existing document, use 185 | ``update()`` method. 186 | 187 | .. code-block:: python 188 | 189 | from google.cloud.firestore import ArrayUnion 190 | data = { 191 | 'cast': ArrayUnion(['Terrence Howard']) 192 | } 193 | 194 | fsdb.collection('Marvels').document('Movies').update(data) 195 | .. 196 | 197 | 198 | Delete Data 199 | ----------- 200 | 201 | |delete-update| 202 | ^^^^^^^^^^^^^^^ 203 | 204 | .. |delete-update| replace:: 205 | update 206 | 207 | To remove an field from an existing document, use ``update()`` method. 208 | 209 | .. code-block:: python 210 | 211 | from google.cloud.firestore import DELETE_FIELD 212 | data = { 213 | 'prequel': DELETE_FIELD 214 | } 215 | 216 | fsdb.collection('Marvels').document('Movies').update(data) 217 | .. 218 | 219 | 220 | 221 | To remove an item to an array field in an existing document, use 222 | ``update()`` method. 223 | 224 | .. code-block:: python 225 | 226 | from google.cloud.firestore import ArrayRemove 227 | data = { 228 | 'cast': ArrayRemove(['Terrence Howard']) 229 | } 230 | 231 | fsdb.collection('Marvels').document('Movies').update(data) 232 | .. 233 | 234 | 235 | delete 236 | ^^^^^^ 237 | 238 | To remove an existing document in a collection, use ``delete()`` 239 | method. 240 | 241 | .. code-block:: python 242 | 243 | fsdb.collection('Marvels').document('Movies').delete() 244 | .. 245 | 246 | 247 | Complex Queries 248 | --------------- 249 | 250 | order_by 251 | ^^^^^^^^ 252 | 253 | To fetch documents with it's data in a collection ``Marvels``, ordered 254 | of field ``year``-s value. 255 | 256 | .. code-block:: python 257 | 258 | fsdb.collection('Marvels').order_by('year').get() 259 | .. 260 | 261 | 262 | 263 | To order the documents in descending order of field ``year``s value 264 | , add ``direction`` keyword argument. 265 | 266 | .. code-block:: python 267 | 268 | from google.cloud.firestore import Query 269 | 270 | fsdb.collection('Marvels').order_by('year', direction=Query.DESCENDING).get() 271 | .. 272 | 273 | 274 | limit_to_first 275 | ^^^^^^^^^^^^^^ 276 | 277 | To limit the number of documents returned in a query to first *N* 278 | documents, we use ``limit_to_first`` method. 279 | 280 | .. code-block:: python 281 | 282 | docs = fsdb.collection('Marvels').order_by('year', direction='DESCENDING').limit_to_first(2).get() 283 | .. 284 | 285 | .. note:: 286 | `limit_to_first` and `limit_to_last` are mutually 287 | exclusive. Setting `limit_to_first` will drop 288 | previously set `limit_to_last`. 289 | 290 | 291 | limit_to_last 292 | ^^^^^^^^^^^^^ 293 | 294 | To limit the number of documents returned in a query to last *N* 295 | documents, we use ``limit_to_last`` method. 296 | 297 | .. code-block:: python 298 | 299 | docs = fsdb.collection('Marvels').order_by('year', direction='ASCENDING').limit_to_last(2).get() 300 | .. 301 | 302 | .. note:: 303 | `limit_to_first` and `limit_to_last` are mutually 304 | exclusive. Setting `limit_to_first` will drop 305 | previously set `limit_to_last`. 306 | 307 | 308 | start_at 309 | ^^^^^^^^ 310 | 311 | To fetch documents with field ``year`` with a ``2007`` or higher will 312 | be fetched from a collection ``Marvels``, and anything before ``2007`` 313 | will be ignored. 314 | 315 | .. code-block:: python 316 | 317 | docs = fsdb.collection('Marvels').order_by('year').start_at({'year': 2007}).get() 318 | .. 319 | 320 | 321 | start_after 322 | ^^^^^^^^^^^ 323 | 324 | To fetch documents with field ``year`` with a value greater than 325 | ``2007`` will be fetched from a collection ``Marvels``, and any 326 | document with a value ``2007`` or less will be ignored. 327 | 328 | .. code-block:: python 329 | 330 | docs = fsdb.collection('Marvels').order_by('year').start_after({'year': 2007}).get() 331 | .. 332 | 333 | 334 | end_at 335 | ^^^^^^ 336 | 337 | To fetch documents with field ``year`` with a ``2022`` or less will 338 | be fetched from a collection ``Marvels``, and anything after ``2022`` 339 | will be ignored. 340 | 341 | .. code-block:: python 342 | 343 | docs = fsdb.collection('Marvels').order_by('year').end_at({'year': 2022}).get() 344 | .. 345 | 346 | 347 | end_before 348 | ^^^^^^^^^^ 349 | 350 | To fetch documents with field ``year`` with a value less than 351 | ``2023`` will be fetched from a collection ``Marvels``, and any 352 | document with a value ``2023`` or greater will be ignored. 353 | 354 | .. code-block:: python 355 | 356 | docs = fsdb.collection('Marvels').order_by('year').end_before({'year': 2007}).get() 357 | .. 358 | 359 | 360 | offset 361 | ^^^^^^ 362 | 363 | To filter out the first *N* documents from a query in collection 364 | ``Marvels``. 365 | 366 | .. code-block:: python 367 | 368 | docs = fsdb.collection('Marvels').order_by('year').offset(5).get() 369 | .. 370 | 371 | 372 | select 373 | ^^^^^^ 374 | 375 | To filter the fields ``lead.nam`` and ``released`` to be returned from 376 | documents in collection ``Marvels``. 377 | 378 | .. code-block:: python 379 | 380 | docs = fsdb.collection('Marvels').select(['lead.name', 'released']).get() 381 | .. 382 | 383 | 384 | where 385 | ^^^^^ 386 | 387 | To fetch all documents and its data in a collection ``Marvels`` where 388 | a field ``year`` exists with a value less than ``2008``. 389 | 390 | .. code-block:: python 391 | 392 | fsdb.collection('Marvels').where('year', '<', 2008).get() 393 | .. 394 | 395 | 396 | 397 | To fetch all documents and its data in a collection ``Marvels`` where 398 | a field ``year`` exists with a value less than equal to ``2008``. 399 | 400 | .. code-block:: python 401 | 402 | fsdb.collection('Marvels').where('year', '<=', 2008).get() 403 | .. 404 | 405 | 406 | 407 | To fetch all documents and its data in a collection ``Marvels`` where 408 | a field ``released`` exists with a value equal to ``True``. 409 | 410 | .. code-block:: python 411 | 412 | fsdb.collection('Marvels').where('released', '==', True).get() 413 | .. 414 | 415 | 416 | 417 | To fetch all documents and its data in a collection ``Marvels`` where 418 | a field ``released`` exists with a value not equal to ``False``. 419 | 420 | .. code-block:: python 421 | 422 | fsdb.collection('Marvels').where('released', '!=', False).get() 423 | .. 424 | 425 | 426 | 427 | To fetch all documents and its data in a collection ``Marvels`` where 428 | a field ``year`` exists with a value greater than equal to ``2008``. 429 | 430 | .. code-block:: python 431 | 432 | fsdb.collection('Marvels').where('year', '>=', 2008).get() 433 | .. 434 | 435 | 436 | To fetch all documents and its data in a collection ``Marvels`` where 437 | a field ``year`` exists with a value greater than ``2008``. 438 | 439 | .. code-block:: python 440 | 441 | fsdb.collection('Marvels').where('year', '>', 2008).get() 442 | .. 443 | 444 | 445 | 446 | To fetch all documents and its data in a collection ``Marvels`` where 447 | a array field ``cast`` exists and contains a value ``Gwyneth Paltrow``. 448 | 449 | .. code-block:: python 450 | 451 | fsdb.collection('Marvels').where('cast', 'array_contains', 'Gwyneth Paltrow').get() 452 | .. 453 | 454 | 455 | 456 | To fetch all documents and its data in a collection ``Marvels`` where 457 | a array field ``cast`` exists and contains either ``Gwyneth Paltrow`` 458 | or ``Terrence Howard`` as a value. 459 | 460 | .. code-block:: python 461 | 462 | fsdb.collection('Marvels').where('cast', 'array_contains_any', ['Gwyneth Paltrow', 'Terrence Howard']).get() 463 | .. 464 | 465 | 466 | 467 | To fetch all documents and its data in a collection ``Marvels`` where 468 | a field ``lead.name`` exists with a value ``Robert Downey Jr.`` or 469 | ``Benedict Cumberbatch``. 470 | 471 | .. code-block:: python 472 | 473 | fsdb.collection('Marvels').where('lead.name', 'in', ['Robert Downey Jr.', 'Benedict Cumberbatch']).get() 474 | .. 475 | 476 | 477 | 478 | To fetch all documents and its data in a collection ``Marvels`` where 479 | a field ``lead.name`` exists without a value ``Robert Downey Jr.`` or 480 | ``Benedict Cumberbatch``. 481 | 482 | .. code-block:: python 483 | 484 | fsdb.collection('Marvels').where('lead.name', 'not-in', ['Robert Downey Jr.', 'Benedict Cumberbatch']).get() 485 | .. 486 | 487 | 488 | 489 | To fetch all documents and its data in a collection ``Marvels`` where 490 | a array field ``cast`` exists with a value ``Gwyneth Paltrow``. 491 | 492 | .. code-block:: python 493 | 494 | fsdb.collection('Marvels').where('cast', 'in', [['Gwyneth Paltrow']]).get() 495 | .. 496 | -------------------------------------------------------------------------------- /docs/guide/setup.rst: -------------------------------------------------------------------------------- 1 | Setup Project 2 | ============= 3 | 4 | Before you can add Firebase to your Python app, you need to 5 | create a Firebase project and register your app with that 6 | project. When you register your app with Firebase, you'll 7 | get a Firebase configuration object that you'll use to 8 | connect your app with your Firebase project resources. 9 | 10 | 11 | Create a Firebase project 12 | ------------------------- 13 | 14 | 1. In the `Firebase console`_, click **Add project**. 15 | 16 | * To add Firebase resources to an existing Google Cloud project, 17 | enter its project name or select it from the dropdown menu. 18 | 19 | * To create a new project, enter the desired project name. You 20 | can also optionally edit the project ID displayed below the 21 | project name. 22 | 23 | .. attention:: 24 | 25 | Firebase generates a unique ID for your Firebase project based 26 | upon the name you give it. If you want to edit this project 27 | ID, you must do it now as it cannot be altered after Firebase 28 | provisions resources for your project. Visit 29 | `Understand Firebase`_ Projects to learn about how Firebase 30 | uses the project ID. 31 | 32 | 2. If prompted, review and accept the Firebase terms. 33 | 34 | 3. Click **Continue**. 35 | 36 | 4. (Optional) Set up Google Analytics for your project. 37 | 38 | .. note:: 39 | 40 | You can always set up Google Analytics later in the 41 | |integrations|_ tab of your :fa:`gear; 2em` Project settings. 42 | 43 | 5. Click **Create project** (or **Add Firebase**, if you're using 44 | an existing Google Cloud project). 45 | 46 | Firebase automatically provisions resources for your Firebase project. 47 | When the process completes, you'll be taken to the overview page for 48 | your Firebase project in the Firebase console. 49 | 50 | .. _Firebase Console: https://console.firebase.google.com 51 | .. |integrations| replace:: *integrations* 52 | .. _integrations: https://console.firebase.google.com/u/0/project/_/settings/integrations 53 | .. _Understand Firebase: https://firebase.google.com/docs/projects/learn-more#project-id 54 | 55 | 56 | Setup Realtime Database 57 | ^^^^^^^^^^^^^^^^^^^^^^^ 58 | 59 | ``databaseURL`` key is not present by default in the Firebase 60 | configuration when an app is :ref:`registered<guide/setup:Register your app>`. 61 | It is recommended to setup database before 62 | :ref:`registering an app<guide/setup:Register your app>`. 63 | 64 | 65 | 66 | Register your app 67 | ----------------- 68 | 69 | After you have a Firebase project, you can register your web app with 70 | that project. 71 | 72 | 1. In the center of the `Firebase console's project overview page`_, 73 | click the **Web** icon (:fa:`code; fa-solid; 2em`) to launch the 74 | setup workflow. 75 | 76 | | If you've already added an app to your Firebase project, 77 | click **Add app** to display the platform options. 78 | 79 | 2. | Enter your app's nickname. 80 | | This nickname is an internal, convenience identifier 81 | and is only visible to you in the Firebase console. 82 | 83 | 3. Click **Register app**. 84 | 85 | 4. | Copy the Firebase configuration dict shown in the screen, and 86 | store it use to connect to your project later in code example 87 | part. 88 | | The dict should be of the architecture shown below: 89 | 90 | .. code-block:: python 91 | 92 | config = { 93 | "apiKey": "apiKey", 94 | "authDomain": "projectId.firebaseapp.com", 95 | "databaseURL": "https://databaseName.firebaseio.com", 96 | "projectId": "projectId", 97 | "storageBucket": "projectId.appspot.com", 98 | "messagingSenderId": "messagingSenderId", 99 | "appId": "appId" 100 | } 101 | .. 102 | 103 | 5. Click **Continue to console**. 104 | 105 | .. _Firebase console's project overview page: https://console.firebase.google.com 106 | -------------------------------------------------------------------------------- /docs/guide/storage.rst: -------------------------------------------------------------------------------- 1 | Storage 2 | ======= 3 | 4 | The storage service allows you to upload files (eg. text, image, 5 | video) to Firebase Storage. 6 | 7 | .. code-block:: python 8 | 9 | # Create storage instance 10 | storage = firebaseApp.storage() 11 | .. 12 | 13 | 14 | child 15 | ----- 16 | 17 | Just like with the Database service, you can build paths to your data 18 | with the Storage service. 19 | 20 | .. code-block:: python 21 | 22 | storage.child("images/example.jpg") 23 | 24 | # Alternative ways 25 | storage.child("images").child("example.jpg") 26 | storage.child("images", "example.jpg") 27 | .. 28 | 29 | put 30 | --- 31 | 32 | The put method takes the path to the local file and an optional user 33 | token. 34 | 35 | .. code-block:: python 36 | 37 | # as admin 38 | storage.child("images/example.jpg").put("example2.jpg") 39 | 40 | # as user 41 | storage.child("images/example.jpg").put("example2.jpg", user['idToken']) 42 | .. 43 | 44 | download 45 | -------- 46 | 47 | The download method takes the path to the saved database file and the 48 | name you want the downloaded file to have. 49 | 50 | .. code-block:: python 51 | 52 | # as admin 53 | storage.child("images/example.jpg").download("downloaded.jpg") 54 | 55 | # as user 56 | storage.child("images/example.jpg").download("downloaded.jpg", user['idToken']) 57 | .. 58 | 59 | get_url 60 | ------- 61 | 62 | The get_url method takes the path to the saved database file and user 63 | token which returns the storage url. 64 | 65 | .. code-block:: python 66 | 67 | # as admin 68 | storage.child("images/example.jpg").get_url() 69 | 70 | # as admin with expiration time for link to expire 71 | storage.child("images/example.jpg").get_url(expiration_hour=12) 72 | 73 | # as user 74 | storage.child("images/example.jpg").get_url(user["idToken"]) 75 | 76 | # returned URL example: 77 | # https://firebasestorage.googleapis.com/v0/b/storage-url.appspot.com/o/images%2Fexample.jpg?alt=media&token=$token 78 | .. 79 | 80 | delete 81 | ------ 82 | 83 | The delete method takes the path to the saved database file and user 84 | token. 85 | 86 | .. code-block:: python 87 | 88 | # as admin 89 | storage.child("images/example.jpg").delete() 90 | 91 | # as user 92 | storage.child("images/example.jpg").delete(user["idToken"]) 93 | .. 94 | 95 | list_of_files 96 | ------------- 97 | 98 | The list_of_files method works only if used under admin credentials. 99 | 100 | .. code-block:: python 101 | 102 | # as admin 103 | storage.list_of_files() 104 | .. 105 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. firebase-rest-api documentation master file, created by 2 | sphinx-quickstart on Thu Jul 7 11:47:19 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Firebase REST API |release| 7 | ########################### 8 | 9 | A simple python wrapper for Google's 10 | `Firebase <https://firebase.google.com>`__ REST API's. 11 | 12 | 13 | Installation 14 | ************ 15 | 16 | .. code-block:: python 17 | 18 | pip install firebase-rest-api 19 | .. 20 | 21 | 22 | 23 | Quick Start 24 | *********** 25 | 26 | In order to use this library, you first need to go through the 27 | following steps: 28 | 29 | 1. Select or create a Firebase project from `Firebase Console`_. 30 | :ref:`(guide)<guide/setup:Create a Firebase project>` 31 | 32 | 2. Register an Web App. 33 | :ref:`(guide)<guide/setup:Register your app>` 34 | 35 | .. _Firebase Console: https://console.firebase.google.com 36 | 37 | 38 | 39 | Example Usage 40 | ============= 41 | 42 | .. code-block:: python 43 | 44 | # Import Firebase REST API library 45 | import firebase 46 | 47 | # Firebase configuration 48 | config = { 49 | "apiKey": "apiKey", 50 | "authDomain": "projectId.firebaseapp.com", 51 | "databaseURL": "https://databaseName.firebaseio.com", 52 | "projectId": "projectId", 53 | "storageBucket": "projectId.appspot.com", 54 | "messagingSenderId": "messagingSenderId", 55 | "appId": "appId" 56 | } 57 | 58 | # Instantiates a Firebase app 59 | app = firebase.initialize_app(config) 60 | 61 | 62 | # Firebase Authentication 63 | auth = app.auth() 64 | 65 | # Create new user and sign in 66 | auth.create_user_with_email_and_password(email, password) 67 | user = auth.sign_in_with_email_and_password(email, password) 68 | 69 | 70 | # Firebase Realtime Database 71 | db = app.database() 72 | 73 | # Data to save in database 74 | data = { 75 | "name": "Robert Downey Jr.", 76 | "email": user.get('email') 77 | } 78 | 79 | # Store data to Firebase Database 80 | db.child("users").push(data, user.get('idToken')) 81 | 82 | 83 | # Firebase Storage 84 | storage = app.storage() 85 | 86 | # File to store in storage 87 | file_path = 'static/img/example.png' 88 | 89 | # Store file to Firebase Storage 90 | storage.child(user.get('email')).child('uploaded-picture.png').put(file_path, user.get('idToken')) 91 | 92 | .. 93 | 94 | 95 | 96 | Documentation contents 97 | ###################### 98 | 99 | .. toctree:: 100 | :maxdepth: 1 101 | 102 | guide/setup 103 | guide/firebase-rest-api 104 | guide/authentication 105 | guide/database 106 | guide/firestore 107 | guide/storage 108 | 109 | .. toctree:: 110 | :maxdepth: 2 111 | 112 | firebase/modules 113 | 114 | 115 | 116 | Indices and tables 117 | ################## 118 | 119 | * :ref:`genindex` 120 | * :ref:`modindex` 121 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=5.0.2 2 | sphinx-rtd-theme>=1.0.0 3 | sphinx_design>=0.2.0 4 | toml>=0.10.2 5 | -------------------------------------------------------------------------------- /firebase/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | """A simple python wrapper for Google's `Firebase`_ REST APIs. 9 | 10 | .. _Firebase: https://firebase.google.com/ 11 | """ 12 | 13 | from .auth import Auth 14 | from .storage import Storage 15 | from .database import Database 16 | from .firestore import Firestore 17 | from ._custom_requests import _custom_request 18 | from ._service_account_credentials import _service_account_creds_from_secret 19 | 20 | 21 | def initialize_app(config): 22 | """Initializes and returns a new Firebase instance. 23 | 24 | :type config: dict 25 | :param config: Firebase configuration 26 | 27 | :return: A newly initialized instance of Firebase. 28 | :rtype: Firebase 29 | """ 30 | 31 | return Firebase(config) 32 | 33 | 34 | class Firebase: 35 | """ Firebase Interface 36 | 37 | :type config: dict 38 | :param config: Firebase configuration 39 | """ 40 | 41 | def __init__(self, config): 42 | """ Constructor """ 43 | 44 | self.api_key = config["apiKey"] 45 | self.auth_domain = config["authDomain"] 46 | self.database_url = config["databaseURL"] 47 | self.project_id = config["projectId"] 48 | self.storage_bucket = config["storageBucket"] 49 | 50 | self.credentials = None 51 | self.requests = _custom_request() 52 | 53 | if config.get("serviceAccount"): 54 | self.credentials = _service_account_creds_from_secret(config['serviceAccount']) 55 | 56 | def auth(self, client_secret=None): 57 | """Initializes and returns a new Firebase Authentication 58 | instance. 59 | 60 | :type client_secret: str or dict 61 | :param client_secret: (Optional) File path to or the dict 62 | object from social client secret file, defaults to 63 | :data:`None`. 64 | 65 | 66 | :return: A newly initialized instance of Auth. 67 | :rtype: Auth 68 | """ 69 | 70 | return Auth(self.api_key, self.credentials, self.requests, client_secret=client_secret) 71 | 72 | def database(self): 73 | """Initializes and returns a new Firebase Realtime Database 74 | instance. 75 | 76 | :return: A newly initialized instance of Database. 77 | :rtype: Database 78 | """ 79 | 80 | return Database(self.credentials, self.database_url, self.requests) 81 | 82 | def firestore(self): 83 | """Initializes and returns a new Firebase Cloud Firestore 84 | instance. 85 | 86 | :return: A newly initialized instance of Firestore. 87 | :rtype: Firestore 88 | """ 89 | 90 | return Firestore(self.api_key, self.credentials, self.project_id, self.requests) 91 | 92 | def storage(self): 93 | """Initializes and returns a new Firebase Storage instance. 94 | 95 | :return: A newly initialized instance of Storage. 96 | :rtype: Storage 97 | """ 98 | 99 | return Storage(self.credentials, self.requests, self.storage_bucket) 100 | -------------------------------------------------------------------------------- /firebase/_custom_requests.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from requests import Session 9 | from requests import adapters 10 | 11 | 12 | def _custom_request(max_retries=None): 13 | """ Custom Session with N retries. 14 | 15 | Incase a request was not completed successfully due to minor 16 | errors, such as connection failures due to unreliable networks; 17 | or server side overload to proceed the request, retrying the 18 | request couple of times might solve it, without raising an error. 19 | 20 | if no value is sent through the function, 21 | `max_retries` is set to 3. 22 | 23 | :param max_retries: number of retries. 24 | :type max_retries: int | None 25 | :return: custom session 26 | :rtype: Session 27 | """ 28 | 29 | session = Session() 30 | adapter = adapters.HTTPAdapter(max_retries=max_retries) 31 | 32 | for scheme in ('http://', 'https://'): 33 | session.mount(scheme, adapter) 34 | 35 | return session 36 | -------------------------------------------------------------------------------- /firebase/_exception.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from requests.exceptions import HTTPError 9 | 10 | 11 | def raise_detailed_error(request_object): 12 | try: 13 | request_object.raise_for_status() 14 | except HTTPError as e: 15 | # raise detailed error message 16 | # TODO: Check if we get a { "error" : "Permission denied." } and handle automatically 17 | raise HTTPError(e, request_object.text) 18 | -------------------------------------------------------------------------------- /firebase/_service_account_credentials.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from google.oauth2.service_account import Credentials 9 | 10 | 11 | def _service_account_creds_from_secret(service_account_secret): 12 | """ Service Account Credentials from Service Account Secrets. 13 | 14 | File path of the service account secret file 15 | in json format can also be passed as the value 16 | of the parameter `service_account_secret`. 17 | 18 | :param service_account_secret: Service Account Secret Key from Firebase Console. 19 | :type service_account_secret: dict | str 20 | :return: Service Account Credentials 21 | :rtype: :class:`~google.oauth2.service_account.Credentials` 22 | """ 23 | 24 | credentials = None 25 | scopes = [ 26 | 'https://www.googleapis.com/auth/firebase.database', 27 | "https://www.googleapis.com/auth/datastore", 28 | 'https://www.googleapis.com/auth/userinfo.email', 29 | "https://www.googleapis.com/auth/cloud-platform" 30 | ] 31 | 32 | if type(service_account_secret) is str: 33 | credentials = Credentials.from_service_account_file(service_account_secret, scopes=scopes) 34 | if type(service_account_secret) is dict: 35 | credentials = Credentials.from_service_account_info(service_account_secret, scopes=scopes) 36 | 37 | return credentials 38 | -------------------------------------------------------------------------------- /firebase/database/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | """ 9 | A simple python wrapper for Google's `Firebase Database REST API`_ 10 | 11 | .. _Firebase Database REST API: 12 | https://firebase.google.com/docs/reference/rest/database 13 | """ 14 | 15 | import math 16 | import json 17 | import time 18 | from random import randrange 19 | from urllib.parse import urlencode 20 | from google.auth.transport.requests import Request 21 | 22 | from ._stream import Stream 23 | from ._db_convert import FirebaseResponse 24 | from firebase._exception import raise_detailed_error 25 | from ._db_convert import convert_to_firebase, convert_list_to_firebase 26 | 27 | 28 | class Database: 29 | """ Firebase Database Service 30 | 31 | 32 | :type credentials: :class:`~google.oauth2.service_account.Credentials` 33 | :param credentials: Service Account Credentials. 34 | 35 | :type database_url: str 36 | :param database_url: ``databaseURL`` from Firebase configuration. 37 | 38 | :type requests: :class:`~requests.Session` 39 | :param requests: Session to make HTTP requests. 40 | """ 41 | 42 | def __init__(self, credentials, database_url, requests): 43 | """ Constructor """ 44 | 45 | if not database_url.endswith('/'): 46 | url = ''.join([database_url, '/']) 47 | else: 48 | url = database_url 49 | 50 | self.credentials = credentials 51 | self.database_url = url 52 | self.requests = requests 53 | 54 | self.path = "" 55 | self.build_query = {} 56 | self.last_push_time = 0 57 | self.last_rand_chars = [] 58 | 59 | def order_by_key(self): 60 | """ Filter data by their keys. 61 | 62 | | For more details: 63 | | |filtering_by_key|_ 64 | 65 | .. |filtering_by_key| replace:: 66 | Firebase Documentation | Retrieve Data | Filtering 67 | Data | filtering_by_key 68 | 69 | .. _filtering_by_key: 70 | https://firebase.google.com/docs/database/rest/retrieve-data#filtering-by-key 71 | 72 | 73 | :return: A reference to the instance object. 74 | :rtype: Database 75 | """ 76 | 77 | self.build_query["orderBy"] = "$key" 78 | 79 | return self 80 | 81 | def order_by_value(self): 82 | """ Filter data by the value of their child keys. 83 | 84 | | For more details: 85 | | |filtering-by-value|_ 86 | 87 | .. |filtering-by-value| replace:: 88 | Firebase Documentation | Retrieve Data | Filtering 89 | Data | filtering-by-value 90 | 91 | .. _filtering-by-value: 92 | https://firebase.google.com/docs/database/rest/retrieve-data#filtering-by-value 93 | 94 | 95 | :return: A reference to the instance object. 96 | :rtype: Database 97 | """ 98 | 99 | self.build_query["orderBy"] = "$value" 100 | 101 | return self 102 | 103 | def order_by_child(self, order): 104 | """ Filter data by a common child key. 105 | 106 | | For more details: 107 | | |filtering-by-a-specified-child-key|_ 108 | 109 | .. |filtering-by-a-specified-child-key| replace:: 110 | Firebase Documentation | Retrieve Data | Filtering 111 | Data | filtering-by-a-specified-child-key 112 | 113 | .. _filtering-by-a-specified-child-key: 114 | https://firebase.google.com/docs/database/rest/retrieve-data#filtering-by-a-specified-child-key 115 | 116 | 117 | :type order: str 118 | :param order: Child key name. 119 | 120 | 121 | :return: A reference to the instance object. 122 | :rtype: Database 123 | """ 124 | 125 | self.build_query["orderBy"] = order 126 | 127 | return self 128 | 129 | def start_at(self, start): 130 | """ Filter data where child key value starts from specified 131 | value. 132 | 133 | | For more details: 134 | | |range-queries|_ 135 | 136 | .. |range-queries| replace:: 137 | Firebase Documentation | Retrieve Data | Complex 138 | Filtering | range-queries 139 | 140 | .. _range-queries: 141 | https://firebase.google.com/docs/database/rest/retrieve-data#range-queries 142 | 143 | 144 | :type start: int or float or str 145 | :param start: Arbitrary starting points for queries. 146 | 147 | 148 | :return: A reference to the instance object. 149 | :rtype: Database 150 | """ 151 | 152 | self.build_query["startAt"] = start 153 | 154 | return self 155 | 156 | def end_at(self, end): 157 | """ Filter data where child key value ends at specified 158 | value. 159 | 160 | | For more details: 161 | | |range-queries|_ 162 | 163 | 164 | :type end: int or float or str 165 | :param end: Arbitrary ending points for queries. 166 | 167 | 168 | :return: A reference to the instance object. 169 | :rtype: Database 170 | """ 171 | 172 | self.build_query["endAt"] = end 173 | 174 | return self 175 | 176 | def equal_to(self, equal): 177 | """ Filter data where child key value is equal to specified 178 | value. 179 | 180 | | For more details: 181 | | |range-queries|_ 182 | 183 | 184 | :type equal: int or float or str 185 | :param equal: Arbitrary point for queries. 186 | 187 | 188 | :return: A reference to the instance object. 189 | :rtype: Database 190 | """ 191 | 192 | self.build_query["equalTo"] = equal 193 | 194 | return self 195 | 196 | def limit_to_first(self, limit_first): 197 | """ Filter the number of data to receive from top. 198 | 199 | | For more details: 200 | | |limit-queries|_ 201 | 202 | .. |limit-queries| replace:: 203 | Firebase Documentation | Retrieve Data | Complex 204 | Filtering | limit-queries 205 | 206 | .. _limit-queries: 207 | https://firebase.google.com/docs/database/rest/retrieve-data#limit-queries 208 | 209 | 210 | :type limit_first: int 211 | :param limit_first: Maximum number of children to select 212 | from top. 213 | 214 | 215 | :return: A reference to the instance object. 216 | :rtype: Database 217 | """ 218 | 219 | self.build_query["limitToFirst"] = limit_first 220 | 221 | return self 222 | 223 | def limit_to_last(self, limit_last): 224 | """ Filter the number of data to receive from bottom. 225 | 226 | | For more details: 227 | | |limit-queries|_ 228 | 229 | 230 | :type limit_last: int 231 | :param limit_last: Maximum number of children to select 232 | from bottom. 233 | 234 | 235 | :return: A reference to the instance object. 236 | :rtype: Database 237 | """ 238 | 239 | self.build_query["limitToLast"] = limit_last 240 | 241 | return self 242 | 243 | def shallow(self): 244 | """ Limit the depth of the response. 245 | 246 | | For more details: 247 | | |section-param-shallow|_ 248 | 249 | .. |section-param-shallow| replace:: 250 | Firebase Database REST API | Query Parameters | 251 | section-param-shallow | 252 | 253 | .. _section-param-shallow: 254 | https://firebase.google.com/docs/reference/rest/database#section-param-shallow 255 | 256 | 257 | :return: A reference to the instance object. 258 | :rtype: Database 259 | """ 260 | 261 | self.build_query["shallow"] = True 262 | 263 | return self 264 | 265 | def child(self, *args): 266 | """ Build paths to your data. 267 | 268 | 269 | :type args: str 270 | :param args: Positional arguments to build path to database. 271 | 272 | 273 | :return: A reference to the instance object. 274 | :rtype: Database 275 | """ 276 | 277 | new_path = "/".join([str(arg) for arg in args]) 278 | 279 | if self.path: 280 | self.path += "/{}".format(new_path) 281 | 282 | else: 283 | if new_path.startswith("/"): 284 | new_path = new_path[1:] 285 | 286 | self.path = new_path 287 | 288 | return self 289 | 290 | def build_request_url(self, token): 291 | """ Builds Request URL for query. 292 | 293 | 294 | :type token: str 295 | :param token: Firebase Auth User ID Token 296 | 297 | 298 | :return: Request URL 299 | :rtype: str 300 | """ 301 | 302 | parameters = {} 303 | 304 | if token: 305 | parameters['auth'] = token 306 | 307 | for param in list(self.build_query): 308 | if type(self.build_query[param]) is str: 309 | parameters[param] = '"' + self.build_query[param] + '"' 310 | 311 | elif type(self.build_query[param]) is bool: 312 | parameters[param] = "true" if self.build_query[param] else "false" 313 | 314 | else: 315 | parameters[param] = self.build_query[param] 316 | 317 | # reset path and build_query for next query 318 | request_ref = '{0}{1}.json?{2}'.format(self.database_url, self.path, urlencode(parameters)) 319 | 320 | self.path = "" 321 | self.build_query = {} 322 | 323 | return request_ref 324 | 325 | def build_headers(self, token=None): 326 | """ Build Request Header. 327 | 328 | :type token: str 329 | :param token: (Optional) Firebase Auth User ID Token, defaults 330 | to :data:`None`. 331 | 332 | 333 | :return: Request Header. 334 | :rtype: dict 335 | """ 336 | 337 | headers = {"content-type": "application/json; charset=UTF-8"} 338 | 339 | if not token and self.credentials: 340 | 341 | if not self.credentials.valid: 342 | self.credentials.refresh(Request()) 343 | 344 | access_token = self.credentials.token 345 | headers['Authorization'] = 'Bearer ' + access_token 346 | 347 | return headers 348 | 349 | def get(self, token=None, json_kwargs={}): 350 | """ Read data from database. 351 | 352 | | For more details: 353 | | |section-get|_ 354 | 355 | .. |section-get| replace:: 356 | Firebase Database REST API | GET - Reading Data 357 | 358 | .. _section-get: 359 | https://firebase.google.com/docs/reference/rest/database#section-get 360 | 361 | 362 | :type token: str 363 | :param token: (Optional) Firebase Auth User ID Token, defaults 364 | to :data:`None`. 365 | 366 | :type json_kwargs: dict 367 | :param json_kwargs: (Optional) Keyword arguments to send to 368 | :func:`json.dumps` method for deserialization of data, 369 | defaults to :data:`{}` (empty :class:`dict` object). 370 | 371 | 372 | :return: The data associated with the path. 373 | :rtype: dict 374 | """ 375 | 376 | build_query = self.build_query 377 | query_key = self.path.split("/")[-1] 378 | request_ref = self.build_request_url(token) 379 | 380 | # headers 381 | headers = self.build_headers(token) 382 | 383 | # do request 384 | request_object = self.requests.get(request_ref, headers=headers) 385 | 386 | raise_detailed_error(request_object) 387 | request_dict = request_object.json(**json_kwargs) 388 | 389 | # if primitive or simple query return 390 | if isinstance(request_dict, list): 391 | return FirebaseResponse(convert_list_to_firebase(request_dict), query_key) 392 | 393 | if not isinstance(request_dict, dict): 394 | return FirebaseResponse(request_dict, query_key) 395 | 396 | if not build_query: 397 | return FirebaseResponse(convert_to_firebase(request_dict.items()), query_key) 398 | 399 | # return keys if shallow 400 | if build_query.get("shallow"): 401 | return FirebaseResponse(request_dict.keys(), query_key) 402 | 403 | # otherwise sort 404 | sorted_response = None 405 | 406 | if build_query.get("orderBy"): 407 | if build_query["orderBy"] == "$key": 408 | sorted_response = sorted(request_dict.items(), key=lambda item: item[0]) 409 | elif build_query["orderBy"] == "$value": 410 | sorted_response = sorted(request_dict.items(), key=lambda item: item[1]) 411 | else: 412 | sorted_response = sorted(request_dict.items(), key=lambda item: (build_query["orderBy"] in item[1], item[1].get(build_query["orderBy"], ""))) 413 | 414 | return FirebaseResponse(convert_to_firebase(sorted_response), query_key) 415 | 416 | def push(self, data, token=None, json_kwargs={}): 417 | """ Add data to database. 418 | 419 | This method adds a Firebase Push ID at the end of the specified 420 | path, and then adds/stores the data in database, unlike 421 | :meth:`set` which does not use a Firebase Push ID. 422 | 423 | | For more details: 424 | | |section-post|_ 425 | 426 | .. |section-post| replace:: 427 | Firebase Database REST API | POST - Pushing Data 428 | 429 | .. _section-post: 430 | https://firebase.google.com/docs/reference/rest/database#section-post 431 | 432 | 433 | :type data: dict 434 | :param data: Data to be stored in database. 435 | 436 | :type token: str 437 | :param token: (Optional) Firebase Auth User ID Token, defaults 438 | to :data:`None`. 439 | 440 | :type json_kwargs: dict 441 | :param json_kwargs: (Optional) Keyword arguments to send to 442 | :func:`json.dumps` method for serialization of data, 443 | defaults to :data:`{}` (empty :class:`dict` object). 444 | 445 | 446 | :return: Child key (Firebase Push ID) name of the data. 447 | :rtype: dict 448 | """ 449 | 450 | request_ref = self.check_token(self.database_url, self.path, token) 451 | 452 | self.path = "" 453 | 454 | headers = self.build_headers(token) 455 | request_object = self.requests.post(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8")) 456 | 457 | raise_detailed_error(request_object) 458 | 459 | return request_object.json() 460 | 461 | def set(self, data, token=None, json_kwargs={}): 462 | """ Add data to database. 463 | 464 | This method writes the data in database in the specified 465 | path, unlike :meth:`push` which creates a Firebase Push ID then 466 | writes the data to database. 467 | 468 | | For more details: 469 | | |section-put|_ 470 | 471 | .. |section-put| replace:: 472 | Firebase Database REST API | PUT - Writing Data 473 | 474 | .. _section-put: 475 | https://firebase.google.com/docs/reference/rest/database#section-put 476 | 477 | 478 | :type data: dict 479 | :param data: Data to be stored in database. 480 | 481 | :type token: str 482 | :param token: (Optional) Firebase Auth User ID Token, defaults 483 | to :data:`None`. 484 | 485 | :type json_kwargs: dict 486 | :param json_kwargs: (Optional) Keyword arguments to send to 487 | :func:`json.dumps` method for serialization of data, 488 | defaults to :data:`{}` (empty :class:`dict` object). 489 | 490 | 491 | :return: Successful attempt returns the ``data`` specified to 492 | add to database. 493 | :rtype: dict 494 | """ 495 | 496 | request_ref = self.check_token(self.database_url, self.path, token) 497 | 498 | self.path = "" 499 | 500 | headers = self.build_headers(token) 501 | request_object = self.requests.put(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8")) 502 | 503 | raise_detailed_error(request_object) 504 | 505 | return request_object.json() 506 | 507 | def update(self, data, token=None, json_kwargs={}): 508 | """ Update stored data of database. 509 | 510 | | For more details: 511 | | |section-patch|_ 512 | 513 | .. |section-patch| replace:: 514 | Firebase Database REST API | PATCH - Updating Data 515 | 516 | .. _section-patch: 517 | https://firebase.google.com/docs/reference/rest/database#section-patch 518 | 519 | 520 | :type data: dict 521 | :param data: Data to be updated. 522 | 523 | :type token: str 524 | :param token: (Optional) Firebase Auth User ID Token, defaults 525 | to :data:`None`. 526 | 527 | :type json_kwargs: dict 528 | :param json_kwargs: (Optional) Keyword arguments to send to 529 | :func:`json.dumps` method for serialization of data, 530 | defaults to :data:`{}` (empty :class:`dict` object). 531 | 532 | 533 | :return: Successful attempt returns the data specified to 534 | update. 535 | :rtype: dict 536 | """ 537 | 538 | request_ref = self.check_token(self.database_url, self.path, token) 539 | 540 | self.path = "" 541 | 542 | headers = self.build_headers(token) 543 | request_object = self.requests.patch(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8")) 544 | 545 | raise_detailed_error(request_object) 546 | 547 | return request_object.json() 548 | 549 | def remove(self, token=None): 550 | """ Delete data from database. 551 | 552 | | For more details: 553 | | |section-delete|_ 554 | 555 | .. |section-delete| replace:: 556 | Firebase Database REST API | DELETE - Removing Data 557 | 558 | .. _section-delete: 559 | https://firebase.google.com/docs/reference/rest/database#section-delete 560 | 561 | 562 | :type token: str 563 | :param token: (Optional) Firebase Auth User ID Token, defaults 564 | to :data:`None`. 565 | 566 | 567 | :return: Successful attempt returns :data:`None`. 568 | :rtype: :data:`None` 569 | """ 570 | 571 | request_ref = self.check_token(self.database_url, self.path, token) 572 | 573 | self.path = "" 574 | 575 | headers = self.build_headers(token) 576 | request_object = self.requests.delete(request_ref, headers=headers) 577 | 578 | raise_detailed_error(request_object) 579 | 580 | return request_object.json() 581 | 582 | def stream(self, stream_handler, token=None, stream_id=None, is_async=True): 583 | request_ref = self.build_request_url(token) 584 | 585 | return Stream(request_ref, stream_handler, self.build_headers, stream_id, is_async) 586 | 587 | def check_token(self, database_url, path, token): 588 | """ Builds Request URL to write/update/remove data. 589 | 590 | 591 | :type database_url: str 592 | :param database_url: ``databaseURL`` from Firebase 593 | configuration. 594 | 595 | :type path: str 596 | :param path: Path to data. 597 | 598 | :type token: str 599 | :param token: Firebase Auth User ID Token 600 | 601 | 602 | :return: Request URL 603 | :rtype: str 604 | """ 605 | 606 | if token: 607 | return '{0}{1}.json?auth={2}'.format(database_url, path, token) 608 | else: 609 | return '{0}{1}.json'.format(database_url, path) 610 | 611 | def generate_key(self): 612 | """ Generate Firebase's push IDs. 613 | 614 | | For more details: 615 | | |firebase-push-id|_ 616 | 617 | .. |firebase-push-id| replace:: 618 | Firebase Blog | The 2^120 Ways to Ensure Unique Identifiers 619 | 620 | .. _firebase-push-id: 621 | https://firebase.blog/posts/2015/02/the-2120-ways-to-ensure-unique_68 622 | 623 | 624 | :return: Firebase's push IDs 625 | :rtype: str 626 | """ 627 | 628 | push_chars = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz' 629 | 630 | now = int(time.time() * 1000) 631 | duplicate_time = now == self.last_push_time 632 | 633 | self.last_push_time = now 634 | time_stamp_chars = [0] * 8 635 | 636 | for i in reversed(range(0, 8)): 637 | time_stamp_chars[i] = push_chars[now % 64] 638 | now = int(math.floor(now / 64)) 639 | 640 | new_id = "".join(time_stamp_chars) 641 | 642 | if not duplicate_time: 643 | self.last_rand_chars = [randrange(64) for _ in range(12)] 644 | else: 645 | for i in range(0, 11): 646 | 647 | if self.last_rand_chars[i] == 63: 648 | self.last_rand_chars[i] = 0 649 | 650 | self.last_rand_chars[i] += 1 651 | 652 | for i in range(0, 12): 653 | new_id += push_chars[self.last_rand_chars[i]] 654 | 655 | return new_id 656 | 657 | def sort(self, origin, by_key, reverse=False): 658 | """ Further sort data based on a child key value. 659 | 660 | 661 | :type origin: dict 662 | :param origin: Data to be sorted (generally the output from 663 | :meth:`get` method). 664 | 665 | :type by_key: str 666 | :param by_key: Child key name to sort by. 667 | 668 | :type reverse: bool 669 | :param reverse: (Optional) Whether to return data in descending 670 | order, defaults to :data:`False` (data is returned in 671 | ascending order). 672 | 673 | 674 | :return: Sorted version of the data. 675 | :rtype: dict 676 | """ 677 | 678 | # unpack firebase objects 679 | firebases = origin.each() 680 | 681 | new_list = [] 682 | 683 | for firebase in firebases: 684 | new_list.append(firebase.item) 685 | 686 | # sort 687 | data = sorted(dict(new_list).items(), key=lambda item: item[1][by_key], reverse=reverse) 688 | 689 | return FirebaseResponse(convert_to_firebase(data), origin.key()) 690 | 691 | def get_etag(self, token=None): 692 | """ Fetches Firebase ETag at a specified location. 693 | 694 | | For more details: 695 | | |section-cond-etag|_ 696 | 697 | .. |section-cond-etag| replace:: 698 | Firebase Database REST API | Conditional Requests | 699 | #section-cond-etag 700 | 701 | .. _section-cond-etag: 702 | https://firebase.google.com/docs/reference/rest/database#section-cond-etag 703 | 704 | 705 | :type token: str 706 | :param token: (Optional) Firebase Auth User ID Token, defaults 707 | to :data:`None`. 708 | 709 | 710 | :return: Firebase ETag 711 | :rtype: str 712 | """ 713 | 714 | request_ref = self.build_request_url(token) 715 | 716 | headers = self.build_headers(token) 717 | # extra header to get ETag 718 | headers['X-Firebase-ETag'] = 'true' 719 | request_object = self.requests.get(request_ref, headers=headers) 720 | 721 | raise_detailed_error(request_object) 722 | 723 | return request_object.headers['ETag'] 724 | 725 | def conditional_set(self, data, etag, token=None, json_kwargs={}): 726 | """ Conditionally add data to database. 727 | 728 | | For more details: 729 | | |section-expected-responses|_ 730 | 731 | .. |section-expected-responses| replace:: 732 | Firebase Database REST API | Conditional Requests | 733 | section-expected-responses 734 | 735 | .. _section-expected-responses: 736 | https://firebase.google.com/docs/reference/rest/database#section-expected-responses 737 | 738 | 739 | :type data: dict 740 | :param data: Data to be stored in database. 741 | 742 | :type etag: str 743 | :param etag: Unique identifier for specific data at a 744 | specified location. 745 | 746 | :type token: str 747 | :param token: (Optional) Firebase Auth User ID Token, defaults 748 | to :data:`None`. 749 | 750 | :type json_kwargs: dict 751 | :param json_kwargs: (Optional) Keyword arguments to send to 752 | :meth:`json.dumps` methods for serialization of data, 753 | defaults to ``{}`` (empty :class:`dict` object). 754 | 755 | 756 | :return: Successful attempt returns the data specified to store, 757 | failed attempt (due to ETag mismatch) returns the current 758 | ``ETag`` for the specified path. 759 | :rtype: dict 760 | """ 761 | 762 | request_ref = self.check_token(self.database_url, self.path, token) 763 | 764 | self.path = "" 765 | 766 | headers = self.build_headers(token) 767 | headers['if-match'] = etag 768 | 769 | request_object = self.requests.put(request_ref, headers=headers, data=json.dumps(data, **json_kwargs).encode("utf-8")) 770 | 771 | # ETag didn't match, so we should return the correct one for the user to try again 772 | if request_object.status_code == 412: 773 | return {'ETag': request_object.headers['ETag']} 774 | 775 | raise_detailed_error(request_object) 776 | 777 | return request_object.json() 778 | 779 | def conditional_remove(self, etag, token=None): 780 | """ Conditionally delete data from database. 781 | 782 | | For more details: 783 | | |section-expected-responses|_ 784 | 785 | 786 | :type etag: str 787 | :param etag: Unique identifier for specific data at a 788 | specified location. 789 | 790 | :type token: str 791 | :param token: (Optional) Firebase Auth User ID Token, defaults 792 | to :data:`None`. 793 | 794 | 795 | :return: Successful attempt returns :data:`None`, in case of ETag 796 | mismatch an updated ETag for the specific data is 797 | returned in :class:`dict` object 798 | :rtype: :data:`None` 799 | """ 800 | 801 | request_ref = self.check_token(self.database_url, self.path, token) 802 | 803 | self.path = "" 804 | 805 | headers = self.build_headers(token) 806 | headers['if-match'] = etag 807 | request_object = self.requests.delete(request_ref, headers=headers) 808 | 809 | # ETag didn't match, so we should return the correct one for the user to try again 810 | if request_object.status_code == 412: 811 | return {'ETag': request_object.headers['ETag']} 812 | 813 | raise_detailed_error(request_object) 814 | 815 | return request_object.json() 816 | -------------------------------------------------------------------------------- /firebase/database/_closable_sse_client.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | import socket 9 | 10 | from ._custom_sse_client import SSEClient 11 | 12 | 13 | class ClosableSSEClient(SSEClient): 14 | 15 | def __init__(self, *args, **kwargs): 16 | self.should_connect = True 17 | super(ClosableSSEClient, self).__init__(*args, **kwargs) 18 | 19 | def _connect(self): 20 | if self.should_connect: 21 | super(ClosableSSEClient, self)._connect() 22 | else: 23 | raise StopIteration() 24 | 25 | def close(self): 26 | self.should_connect = False 27 | self.retry = 0 28 | 29 | self.resp.raw._fp.fp.raw._sock.shutdown(socket.SHUT_RDWR) 30 | self.resp.raw._fp.fp.raw._sock.close() 31 | -------------------------------------------------------------------------------- /firebase/database/_custom_sse_client.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | import re 9 | import six 10 | import time 11 | import requests 12 | import warnings 13 | 14 | 15 | # Technically, we should support streams that mix line endings. This regex, 16 | # however, assumes that a system will provide consistent line endings. 17 | end_of_field = re.compile(r'\r\n\r\n|\r\r|\n\n') 18 | 19 | 20 | class SSEClient(object): 21 | 22 | def __init__(self, url, session, build_headers, last_id=None, retry=3000, **kwargs): 23 | self.url = url 24 | self.last_id = last_id 25 | self.retry = retry 26 | self.running = True 27 | 28 | # Optional support for passing in a requests.Session() 29 | self.session = session 30 | 31 | # function for building auth header when token expires 32 | self.build_headers = build_headers 33 | self.start_time = None 34 | 35 | # Any extra kwargs will be fed into the requests.get call later. 36 | self.requests_kwargs = kwargs 37 | 38 | # The SSE spec requires making requests with Cache-Control: nocache 39 | if 'headers' not in self.requests_kwargs: 40 | self.requests_kwargs['headers'] = {} 41 | 42 | self.requests_kwargs['headers']['Cache-Control'] = 'no-cache' 43 | 44 | # The 'Accept' header is not required, but explicit > implicit 45 | self.requests_kwargs['headers']['Accept'] = 'text/event-stream' 46 | 47 | # Keep data here as it streams in 48 | self.buf = u'' 49 | 50 | self._connect() 51 | 52 | def _connect(self): 53 | if self.last_id: 54 | self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id 55 | 56 | headers = self.build_headers() 57 | self.requests_kwargs['headers'].update(headers) 58 | 59 | # Use session if set. Otherwise, fall back to `requests` module. 60 | self.requester = self.session or requests 61 | self.resp = self.requester.get(self.url, stream=True, **self.requests_kwargs) 62 | 63 | self.resp_iterator = self.resp.iter_content(decode_unicode=True) 64 | 65 | # TODO: Ensure we're handling redirects. Might also stick the 'origin' 66 | # attribute on Events like the Javascript spec requires. 67 | self.resp.raise_for_status() 68 | 69 | def _event_complete(self): 70 | return re.search(end_of_field, self.buf) is not None 71 | 72 | def __iter__(self): 73 | return self 74 | 75 | def __next__(self): 76 | while not self._event_complete(): 77 | 78 | try: 79 | nextchar = next(self.resp_iterator) 80 | self.buf += nextchar 81 | except (StopIteration, requests.RequestException): 82 | time.sleep(self.retry / 1000.0) 83 | self._connect() 84 | 85 | # The SSE spec only supports resuming from a whole message, so 86 | # if we have half a message we should throw it out. 87 | head, sep, tail = self.buf.rpartition('\n') 88 | self.buf = head + sep 89 | continue 90 | 91 | split = re.split(end_of_field, self.buf) 92 | head = split[0] 93 | tail = "".join(split[1:]) 94 | 95 | self.buf = tail 96 | msg = Event.parse(head) 97 | 98 | if msg.data == "credential is no longer valid": 99 | self._connect() 100 | return None 101 | 102 | if msg.data == 'null': 103 | return None 104 | 105 | # If the server requests a specific retry delay, we need to honor it. 106 | if msg.retry: 107 | self.retry = msg.retry 108 | 109 | # last_id should only be set if included in the message. It's not 110 | # forgotten if a message omits it. 111 | if msg.id: 112 | self.last_id = msg.id 113 | 114 | return msg 115 | 116 | if six.PY2: 117 | next = __next__ 118 | 119 | 120 | class Event(object): 121 | 122 | sse_line_pattern = re.compile('(?P<name>[^:]*):?( ?(?P<value>.*))?') 123 | 124 | def __init__(self, data='', event='message', id=None, retry=None): 125 | self.data = data 126 | self.event = event 127 | self.id = id 128 | self.retry = retry 129 | 130 | def dump(self): 131 | lines = [] 132 | 133 | if self.id: 134 | lines.append('id: %s' % self.id) 135 | 136 | # Only include an event line if it's not the default already. 137 | if self.event != 'message': 138 | lines.append('event: %s' % self.event) 139 | 140 | if self.retry: 141 | lines.append('retry: %s' % self.retry) 142 | 143 | lines.extend('data: %s' % d for d in self.data.split('\n')) 144 | 145 | return '\n'.join(lines) + '\n\n' 146 | 147 | @classmethod 148 | def parse(cls, raw): 149 | """ 150 | Given a possibly-multiline string representing an SSE message, parse it 151 | and return a `Event` object. 152 | """ 153 | msg = cls() 154 | 155 | for line in raw.split('\n'): 156 | m = cls.sse_line_pattern.match(line) 157 | 158 | if m is None: 159 | # Malformed line. Discard but warn. 160 | warnings.warn('Invalid SSE line: "%s"' % line, SyntaxWarning) 161 | continue 162 | 163 | name = m.groupdict()['name'] 164 | value = m.groupdict()['value'] 165 | 166 | if name == '': 167 | # line began with a ":", so is a comment. Ignore 168 | continue 169 | 170 | if name == 'data': 171 | # If we already have some data, then join to it with a newline. 172 | # Else this is it. 173 | if msg.data: 174 | msg.data = '%s\n%s' % (msg.data, value) 175 | else: 176 | msg.data = value 177 | 178 | elif name == 'event': 179 | msg.event = value 180 | 181 | elif name == 'id': 182 | msg.id = value 183 | 184 | elif name == 'retry': 185 | msg.retry = int(value) 186 | 187 | return msg 188 | 189 | def __str__(self): 190 | return self.data 191 | -------------------------------------------------------------------------------- /firebase/database/_db_convert.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from collections import OrderedDict 9 | 10 | 11 | class FirebaseKeyValue: 12 | 13 | def __init__(self, item): 14 | self.item = item 15 | 16 | def val(self): 17 | return self.item[1] 18 | 19 | def key(self): 20 | return self.item[0] 21 | 22 | 23 | class FirebaseResponse: 24 | 25 | def __init__(self, firebases, query_key): 26 | self.firebases = firebases 27 | self.query_key = query_key 28 | 29 | def __getitem__(self, index): 30 | return self.firebases[index] 31 | 32 | def val(self): 33 | if isinstance(self.firebases, list): 34 | 35 | # unpack firebases into OrderedDict 36 | firebase_list = [] 37 | 38 | # if firebase response was a list 39 | if isinstance(self.firebases[0].key(), int): 40 | 41 | for firebase in self.firebases: 42 | firebase_list.append(firebase.val()) 43 | 44 | return firebase_list 45 | 46 | # if firebase response was a dict with keys 47 | for firebase in self.firebases: 48 | firebase_list.append((firebase.key(), firebase.val())) 49 | 50 | return OrderedDict(firebase_list) 51 | 52 | else: 53 | 54 | return self.firebases 55 | 56 | def key(self): 57 | return self.query_key 58 | 59 | def each(self): 60 | if isinstance(self.firebases, list): 61 | return self.firebases 62 | 63 | 64 | def convert_to_firebase(items): 65 | firebase_list = [] 66 | 67 | for item in items: 68 | firebase_list.append(FirebaseKeyValue(item)) 69 | 70 | return firebase_list 71 | 72 | 73 | def convert_list_to_firebase(items): 74 | firebase_list = [] 75 | 76 | for item in items: 77 | firebase_list.append(FirebaseKeyValue([items.index(item), item])) 78 | 79 | return firebase_list 80 | -------------------------------------------------------------------------------- /firebase/database/_keep_auth_session.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from requests import Session 9 | 10 | 11 | class KeepAuthSession(Session): 12 | """ 13 | A session that doesn't drop Authentication on redirects between domains. 14 | """ 15 | 16 | def rebuild_auth(self, prepared_request, response): 17 | pass 18 | -------------------------------------------------------------------------------- /firebase/database/_stream.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | import json 9 | import time 10 | import threading 11 | 12 | from ._keep_auth_session import KeepAuthSession 13 | from ._closable_sse_client import ClosableSSEClient 14 | 15 | 16 | class Stream: 17 | 18 | def __init__(self, url, stream_handler, build_headers, stream_id, is_async): 19 | self.build_headers = build_headers 20 | self.url = url 21 | self.stream_handler = stream_handler 22 | self.stream_id = stream_id 23 | self.sse = None 24 | self.thread = None 25 | 26 | if is_async: 27 | self.start() 28 | else: 29 | self.start_stream() 30 | 31 | def make_session(self): 32 | """ 33 | Return a custom session object to be passed to the ClosableSSEClient. 34 | """ 35 | session = KeepAuthSession() 36 | 37 | return session 38 | 39 | def start(self): 40 | self.thread = threading.Thread(target=self.start_stream) 41 | self.thread.start() 42 | 43 | return self 44 | 45 | def start_stream(self): 46 | self.sse = ClosableSSEClient(self.url, session=self.make_session(), build_headers=self.build_headers) 47 | 48 | for msg in self.sse: 49 | if msg: 50 | msg_data = json.loads(msg.data) 51 | msg_data["event"] = msg.event 52 | 53 | if self.stream_id: 54 | msg_data["stream_id"] = self.stream_id 55 | 56 | self.stream_handler(msg_data) 57 | 58 | def close(self): 59 | while not self.sse and not hasattr(self.sse, 'resp'): 60 | time.sleep(0.001) 61 | 62 | self.sse.running = False 63 | self.sse.close() 64 | self.thread.join() 65 | 66 | return self 67 | -------------------------------------------------------------------------------- /firebase/firestore/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | """ 9 | A simple python wrapper for Google's `Firebase Cloud Firestore REST API`_ 10 | 11 | .. _Firebase Cloud Firestore REST API: 12 | https://firebase.google.com/docs/firestore/reference/rest 13 | """ 14 | 15 | from math import ceil 16 | from proto.message import Message 17 | from google.cloud.firestore import Client 18 | from google.cloud.firestore_v1._helpers import * 19 | from google.cloud.firestore_v1.query import Query 20 | from google.cloud.firestore_v1.collection import CollectionReference 21 | from google.cloud.firestore_v1.base_query import _enum_from_direction 22 | 23 | from ._utils import _from_datastore, _to_datastore 24 | from firebase._exception import raise_detailed_error 25 | 26 | 27 | class Firestore: 28 | """ Firebase Firestore Service 29 | 30 | :type api_key: str 31 | :param api_key: ``apiKey`` from Firebase configuration 32 | 33 | :type credentials: :class:`~google.oauth2.service_account.Credentials` 34 | :param credentials: Service Account Credentials 35 | 36 | :type project_id: str 37 | :param project_id: ``projectId`` from Firebase configuration 38 | 39 | :type requests: :class:`~requests.Session` 40 | :param requests: Session to make HTTP requests 41 | """ 42 | 43 | def __init__(self, api_key, credentials, project_id, requests): 44 | """ Constructor method """ 45 | 46 | self._api_key = api_key 47 | self._credentials = credentials 48 | self._project_id = project_id 49 | self._requests = requests 50 | 51 | def collection(self, collection_id): 52 | """ Get reference to a collection in a Firestore database. 53 | 54 | 55 | :type collection_id: str 56 | :param collection_id: An ID of collection in firestore. 57 | 58 | 59 | :return: Reference to a collection. 60 | :rtype: Collection 61 | """ 62 | 63 | return Collection([collection_id], api_key=self._api_key, credentials=self._credentials, project_id=self._project_id, requests=self._requests) 64 | 65 | 66 | class Collection: 67 | """ A reference to a collection in a Firestore database. 68 | 69 | :type collection_path: list 70 | :param collection_path: Collective form of strings to create a 71 | Collection. 72 | 73 | :type api_key: str 74 | :param api_key: ``apiKey`` from Firebase configuration 75 | 76 | :type credentials: :class:`~google.oauth2.service_account.Credentials` 77 | :param credentials: Service Account Credentials 78 | 79 | :type project_id: str 80 | :param project_id: ``projectId`` from Firebase configuration 81 | 82 | :type requests: :class:`~requests.Session` 83 | :param requests: Session to make HTTP requests 84 | """ 85 | 86 | def __init__(self, collection_path, api_key, credentials, project_id, requests): 87 | """ Constructor method """ 88 | 89 | self._path = collection_path 90 | 91 | self._api_key = api_key 92 | self._credentials = credentials 93 | self._project_id = project_id 94 | self._requests = requests 95 | 96 | self._base_path = f"projects/{self._project_id}/databases/(default)/documents" 97 | self._base_url = f"https://firestore.googleapis.com/v1/{self._base_path}" 98 | 99 | if self._credentials: 100 | self.__datastore = Client(credentials=self._credentials, project=self._project_id) 101 | 102 | self._query = {} 103 | self._is_limited_to_last = False 104 | 105 | def _build_query(self): 106 | """ Builds query for firestore to execute. 107 | 108 | 109 | :return: An query. 110 | :rtype: :class:`~google.cloud.firestore_v1.query.Query` 111 | """ 112 | 113 | if self._credentials: 114 | _query = _build_db(self.__datastore, self._path) 115 | else: 116 | _query = Query(CollectionReference(self._path.pop())) 117 | 118 | for key, val in self._query.items(): 119 | if key == 'endAt': 120 | _query = _query.end_at(val) 121 | elif key == 'endBefore': 122 | _query = _query.end_before(val) 123 | elif key == 'limit': 124 | _query = _query.limit(val) 125 | elif key == 'limitToLast': 126 | _query = _query.limit_to_last(val) 127 | elif key == 'offset': 128 | _query = _query.offset(val) 129 | elif key == 'orderBy': 130 | for q in val: 131 | _query = _query.order_by(q[0], **q[1]) 132 | elif key == 'select': 133 | _query = _query.select(val) 134 | elif key == 'startAfter': 135 | _query = _query.start_after(val) 136 | elif key == 'startAt': 137 | _query = _query.start_at(val) 138 | elif key == 'where': 139 | for q in val: 140 | _query = _query.where(q[0], q[1], q[2]) 141 | 142 | if not self._credentials and _query._limit_to_last: 143 | 144 | self._is_limited_to_last = _query._limit_to_last 145 | 146 | for order in _query._orders: 147 | order.direction = _enum_from_direction( 148 | _query.DESCENDING 149 | if order.direction == _query.ASCENDING 150 | else _query.ASCENDING 151 | ) 152 | 153 | _query._limit_to_last = False 154 | 155 | self._path.clear() 156 | self._query.clear() 157 | 158 | return _query 159 | 160 | def add(self, data, token=None): 161 | """ Create a document in the Firestore database with the 162 | provided data using an auto generated ID for the document. 163 | 164 | 165 | :type data: dict 166 | :param data: Data to be stored in firestore. 167 | 168 | :type token: str 169 | :param token: (Optional) Firebase Auth User ID Token, defaults 170 | to :data:`None`. 171 | 172 | 173 | :return: returns the auto generated document ID, used to store 174 | the data. 175 | :rtype: str 176 | """ 177 | 178 | path = self._path.copy() 179 | self._path.clear() 180 | 181 | if self._credentials: 182 | db_ref = _build_db(self.__datastore, path) 183 | 184 | response = db_ref.add(data) 185 | 186 | return response[1].id 187 | 188 | else: 189 | req_ref = f"{self._base_url}/{'/'.join(path)}?key={self._api_key}" 190 | 191 | if token: 192 | headers = {"Authorization": "Firebase " + token} 193 | response = self._requests.post(req_ref, headers=headers, json=_to_datastore(data)) 194 | 195 | else: 196 | response = self._requests.post(req_ref, json=_to_datastore(data)) 197 | 198 | raise_detailed_error(response) 199 | 200 | doc_id = response.json()['name'].split('/') 201 | 202 | return doc_id.pop() 203 | 204 | def document(self, document_id): 205 | """ A reference to a document in a collection. 206 | 207 | 208 | :type document_id: str 209 | :param document_id: An ID of document inside a collection. 210 | 211 | 212 | :return: Reference to a document. 213 | :rtype: Document 214 | """ 215 | 216 | self._path.append(document_id) 217 | return Document(self._path, api_key=self._api_key, credentials=self._credentials, project_id=self._project_id, requests=self._requests) 218 | 219 | def end_at(self, document_fields): 220 | """ End query at a cursor with this collection as parent. 221 | 222 | 223 | :type document_fields: dict 224 | :param document_fields: A dictionary of fields representing a 225 | query results cursor. A cursor is a collection of values 226 | that represent a position in a query result set. 227 | 228 | 229 | :return: A reference to the instance object. 230 | :rtype: Collection 231 | """ 232 | 233 | self._query['endAt'] = document_fields 234 | 235 | return self 236 | 237 | def end_before(self, document_fields): 238 | """ End query before a cursor with this collection as parent. 239 | 240 | 241 | :type document_fields: dict 242 | :param document_fields: A dictionary of fields representing a 243 | query results cursor. A cursor is a collection of values 244 | that represent a position in a query result set. 245 | 246 | 247 | :return: A reference to the instance object. 248 | :rtype: Collection 249 | """ 250 | 251 | self._query['endBefore'] = document_fields 252 | 253 | return self 254 | 255 | def get(self, token=None): 256 | """ Returns a list of dict's containing document ID and the 257 | data stored within them. 258 | 259 | 260 | :type token: str 261 | :param token: (Optional) Firebase Auth User ID Token, defaults 262 | to :data:`None`. 263 | 264 | 265 | :return: A list of document ID's with the data they possess. 266 | :rtype: list 267 | """ 268 | 269 | docs = [] 270 | 271 | if self._credentials: 272 | db_ref = self._build_query() 273 | 274 | results = db_ref.get() 275 | 276 | for result in results: 277 | docs.append({result.id: result.to_dict()}) 278 | 279 | else: 280 | 281 | body = None 282 | 283 | if len(self._query) > 0: 284 | req_ref = f"{self._base_url}/{'/'.join(self._path[:-1])}:runQuery?key={self._api_key}" 285 | 286 | body = { 287 | "structuredQuery": json.loads(Message.to_json(self._build_query()._to_protobuf())) 288 | } 289 | 290 | else: 291 | req_ref = f"{self._base_url}/{'/'.join(self._path)}?key={self._api_key}" 292 | 293 | if token: 294 | headers = {"Authorization": "Firebase " + token} 295 | 296 | if body: 297 | response = self._requests.post(req_ref, headers=headers, json=body) 298 | else: 299 | response = self._requests.get(req_ref, headers=headers) 300 | 301 | else: 302 | 303 | if body: 304 | response = self._requests.post(req_ref, json=body) 305 | else: 306 | response = self._requests.get(req_ref) 307 | 308 | raise_detailed_error(response) 309 | 310 | if isinstance(response.json(), dict): 311 | for doc in response.json()['documents']: 312 | doc_id = doc['name'].split('/') 313 | docs.append({doc_id.pop(): _from_datastore({'fields': doc['fields']})}) 314 | 315 | elif isinstance(response.json(), list): 316 | for doc in response.json(): 317 | fields = {} 318 | 319 | if doc.get('document'): 320 | 321 | if doc.get('document').get('fields'): 322 | fields = doc['document']['fields'] 323 | 324 | doc_id = doc['document']['name'].split('/') 325 | docs.append({doc_id.pop(): _from_datastore({'fields': fields})}) 326 | 327 | if self._is_limited_to_last: 328 | docs = list(reversed(list(docs))) 329 | 330 | return docs 331 | 332 | def list_of_documents(self, token=None): 333 | """ List all sub-documents of the current collection. 334 | 335 | :type token: str 336 | :param token: (Optional) Firebase Auth User ID Token, defaults 337 | to :data:`None`. 338 | 339 | 340 | :return: A list of document ID's. 341 | :rtype: list 342 | """ 343 | 344 | docs = [] 345 | 346 | path = self._path.copy() 347 | self._path.clear() 348 | 349 | if self._credentials: 350 | db_ref = _build_db(self.__datastore, path) 351 | 352 | list_doc = list(db_ref.list_documents()) 353 | 354 | for doc in list_doc: 355 | docs.append(doc.id) 356 | 357 | else: 358 | 359 | req_ref = f"{self._base_url}/{'/'.join(path)}?key={self._api_key}" 360 | 361 | if token: 362 | headers = {"Authorization": "Firebase " + token} 363 | response = self._requests.get(req_ref, headers=headers) 364 | 365 | else: 366 | response = self._requests.get(req_ref) 367 | 368 | raise_detailed_error(response) 369 | 370 | if response.json().get('documents'): 371 | for doc in response.json()['documents']: 372 | doc_id = doc['name'].split('/') 373 | docs.append(doc_id.pop()) 374 | 375 | return docs 376 | 377 | def limit_to_first(self, count): 378 | """ Create a limited query with this collection as parent. 379 | 380 | .. note:: 381 | `limit_to_first` and `limit_to_last` are mutually 382 | exclusive. Setting `limit_to_first` will drop 383 | previously set `limit_to_last`. 384 | 385 | 386 | :type count: int 387 | :param count: Maximum number of documents to return that match 388 | the query. 389 | 390 | 391 | :return: A reference to the instance object. 392 | :rtype: Collection 393 | """ 394 | 395 | self._query['limit'] = count 396 | 397 | return self 398 | 399 | def limit_to_last(self, count): 400 | """ Create a limited to last query with this collection as 401 | parent. 402 | 403 | .. note:: 404 | `limit_to_first` and `limit_to_last` are mutually 405 | exclusive. Setting `limit_to_first` will drop 406 | previously set `limit_to_last`. 407 | 408 | 409 | :type count: int 410 | :param count: Maximum number of documents to return that 411 | match the query. 412 | 413 | 414 | :return: A reference to the instance object. 415 | :rtype: Collection 416 | """ 417 | 418 | self._query['limitToLast'] = count 419 | 420 | return self 421 | 422 | def offset(self, num_to_skip): 423 | """ Skip to an offset in a query with this collection as parent. 424 | 425 | 426 | :type num_to_skip: int 427 | :param num_to_skip: The number of results to skip at the 428 | beginning of query results. (Must be non-negative.) 429 | 430 | 431 | :return: A reference to the instance object. 432 | :rtype: Collection 433 | """ 434 | 435 | self._query['offset'] = num_to_skip 436 | 437 | return self 438 | 439 | def order_by(self, field_path, **kwargs): 440 | """ Create an "order by" query with this collection as parent. 441 | 442 | 443 | :type field_path: str 444 | :param field_path: A field path (``.``-delimited list of field 445 | names) on which to order the query results. 446 | 447 | :Keyword Arguments: 448 | * *direction* ( :class:`str` ) -- 449 | Sort query results in ascending/descending order on a field. 450 | 451 | 452 | :return: A reference to the instance object. 453 | :rtype: Collection 454 | """ 455 | 456 | arr = [] 457 | 458 | if self._query.get('orderBy'): 459 | arr = self._query['orderBy'] 460 | 461 | arr.append([field_path, kwargs]) 462 | 463 | self._query['orderBy'] = arr 464 | 465 | return self 466 | 467 | def select(self, field_paths): 468 | """ Create a "select" query with this collection as parent. 469 | 470 | :type field_paths: list 471 | :param field_paths: A list of field paths (``.``-delimited list 472 | of field names) to use as a projection of document fields 473 | in the query results. 474 | 475 | 476 | :return: A reference to the instance object. 477 | :rtype: Collection 478 | """ 479 | 480 | self._query['select'] = field_paths 481 | 482 | return self 483 | 484 | def start_after(self, document_fields): 485 | """ Start query after a cursor with this collection as parent. 486 | 487 | 488 | :type document_fields: dict 489 | :param document_fields: A dictionary of fields representing 490 | a query results cursor. A cursor is a collection of values 491 | that represent a position in a query result set. 492 | 493 | 494 | :return: A reference to the instance object. 495 | :rtype: Collection 496 | """ 497 | 498 | self._query['startAfter'] = document_fields 499 | 500 | return self 501 | 502 | def start_at(self, document_fields): 503 | """ Start query at a cursor with this collection as parent. 504 | 505 | 506 | :type document_fields: dict 507 | :param document_fields: A dictionary of fields representing a 508 | query results cursor. A cursor is a collection of values 509 | that represent a position in a query result set. 510 | 511 | 512 | :return: A reference to the instance object. 513 | :rtype: Collection 514 | """ 515 | 516 | self._query['startAt'] = document_fields 517 | 518 | return self 519 | 520 | def where(self, field_path, op_string, value): 521 | """ Create a "where" query with this collection as parent. 522 | 523 | 524 | :type field_path: str 525 | :param field_path: A field path (``.``-delimited list of field 526 | names) for the field to filter on. 527 | 528 | :type op_string: str 529 | :param op_string: A comparison operation in the form of a 530 | string. Acceptable values are ``<``, ``<=``, ``==``, ``!=`` 531 | , ``>=``, ``>``, ``in``, ``not-in``, ``array_contains`` and 532 | ``array_contains_any``. 533 | 534 | :type value: Any 535 | :param value: The value to compare the field against in the 536 | filter. If ``value`` is :data:`None` or a NaN, then ``==`` 537 | is the only allowed operation. If ``op_string`` is ``in``, 538 | ``value`` must be a sequence of values. 539 | 540 | 541 | :return: A reference to the instance object. 542 | :rtype: Collection 543 | """ 544 | 545 | arr = [] 546 | 547 | if self._query.get('where'): 548 | arr = self._query['where'] 549 | 550 | arr.append([field_path, op_string, value]) 551 | 552 | self._query['where'] = arr 553 | 554 | return self 555 | 556 | 557 | class Document: 558 | """ A reference to a document in a Firestore database. 559 | 560 | :type document_path: list 561 | :param document_path: Collective form of strings to create a 562 | Document. 563 | 564 | :type api_key: str 565 | :param api_key: ``apiKey`` from Firebase configuration 566 | 567 | :type credentials: :class:`~google.oauth2.service_account.Credentials` 568 | :param credentials: Service Account Credentials 569 | 570 | :type project_id: str 571 | :param project_id: ``projectId`` from Firebase configuration 572 | 573 | :type requests: :class:`~requests.Session` 574 | :param requests: Session to make HTTP requests 575 | """ 576 | 577 | def __init__(self, document_path, api_key, credentials, project_id, requests): 578 | """ Constructor method """ 579 | 580 | self._path = document_path 581 | 582 | self._api_key = api_key 583 | self._credentials = credentials 584 | self._project_id = project_id 585 | self._requests = requests 586 | 587 | self._base_path = f"projects/{self._project_id}/databases/(default)/documents" 588 | self._base_url = f"https://firestore.googleapis.com/v1/{self._base_path}" 589 | 590 | if self._credentials: 591 | self.__datastore = Client(credentials=self._credentials, project=self._project_id) 592 | 593 | def collection(self, collection_id): 594 | """ A reference to a collection in a Firestore database. 595 | 596 | 597 | :type collection_id: str 598 | :param collection_id: An ID of collection in firestore. 599 | 600 | 601 | :return: Reference to a collection. 602 | :rtype: Collection 603 | """ 604 | 605 | self._path.append(collection_id) 606 | return Collection(self._path, api_key=self._api_key, credentials=self._credentials, project_id=self._project_id, requests=self._requests) 607 | 608 | def delete(self, token=None): 609 | """ Deletes the current document from firestore. 610 | 611 | | For more details: 612 | | |delete_documents|_ 613 | 614 | .. |delete_documents| replace:: 615 | Firebase Documentation | Delete data from Cloud 616 | Firestore | Delete documents 617 | 618 | .. _delete_documents: 619 | https://firebase.google.com/docs/firestore/manage-data/delete-data#delete_documents 620 | 621 | :type token: str 622 | :param token: (Optional) Firebase Auth User ID Token, defaults 623 | to :data:`None`. 624 | """ 625 | 626 | path = self._path.copy() 627 | self._path.clear() 628 | 629 | if self._credentials: 630 | db_ref = _build_db(self.__datastore, path) 631 | 632 | db_ref.delete() 633 | 634 | else: 635 | req_ref = f"{self._base_url}/{'/'.join(path)}?key={self._api_key}" 636 | 637 | if token: 638 | headers = {"Authorization": "Firebase " + token} 639 | response = self._requests.delete(req_ref, headers=headers) 640 | 641 | else: 642 | response = self._requests.delete(req_ref) 643 | 644 | raise_detailed_error(response) 645 | 646 | def get(self, field_paths=None, token=None): 647 | """ Read data from a document in firestore. 648 | 649 | 650 | :type field_paths: list 651 | :param field_paths: (Optional) A list of field paths 652 | (``.``-delimited list of field names) to filter data, and 653 | return the filtered values only, defaults 654 | to :data:`None`. 655 | 656 | :type token: str 657 | :param token: (Optional) Firebase Auth User ID Token, defaults 658 | to :data:`None`. 659 | 660 | 661 | :return: The whole data stored in the document unless filtered 662 | to retrieve specific fields. 663 | :rtype: dict 664 | """ 665 | 666 | path = self._path.copy() 667 | self._path.clear() 668 | 669 | if self._credentials: 670 | db_ref = _build_db(self.__datastore, path) 671 | 672 | result = db_ref.get(field_paths=field_paths) 673 | 674 | return result.to_dict() 675 | 676 | else: 677 | 678 | mask = '' 679 | 680 | if field_paths: 681 | for field_path in field_paths: 682 | mask = f"{mask}mask.fieldPaths={field_path}&" 683 | 684 | req_ref = f"{self._base_url}/{'/'.join(path)}?{mask}key={self._api_key}" 685 | 686 | if token: 687 | headers = {"Authorization": "Firebase " + token} 688 | response = self._requests.get(req_ref, headers=headers) 689 | 690 | else: 691 | response = self._requests.get(req_ref) 692 | 693 | raise_detailed_error(response) 694 | 695 | return _from_datastore(response.json()) 696 | 697 | def set(self, data, token=None): 698 | """ Add data to a document in firestore. 699 | 700 | | For more details: 701 | | |set_a_document|_ 702 | 703 | .. |set_a_document| replace:: 704 | Firebase Documentation | Add data to Cloud Firestore | Set 705 | a document 706 | 707 | .. _set_a_document: 708 | https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document 709 | 710 | 711 | :type data: dict 712 | :param data: Data to be stored in firestore. 713 | 714 | :type token: str 715 | :param token: (Optional) Firebase Auth User ID Token, defaults 716 | to :data:`None`. 717 | """ 718 | 719 | path = self._path.copy() 720 | self._path.clear() 721 | 722 | if self._credentials: 723 | db_ref = _build_db(self.__datastore, path) 724 | 725 | db_ref.set(data) 726 | 727 | else: 728 | 729 | req_ref = f"{self._base_url}:commit?key={self._api_key}" 730 | 731 | body = { 732 | "writes": [ 733 | Message.to_dict(pbs_for_set_no_merge(f"{self._base_path}/{'/'.join(path)}", data)[0]) 734 | ] 735 | } 736 | 737 | if token: 738 | headers = {"Authorization": "Firebase " + token} 739 | response = self._requests.post(req_ref, headers=headers, json=body) 740 | 741 | else: 742 | response = self._requests.post(req_ref, json=body) 743 | 744 | raise_detailed_error(response) 745 | 746 | def update(self, data, token=None): 747 | """ Update stored data inside a document in firestore. 748 | 749 | 750 | :type data: dict 751 | :param data: Data to be stored in firestore. 752 | 753 | :type token: str 754 | :param token: (Optional) Firebase Auth User ID Token, defaults 755 | to :data:`None`. 756 | """ 757 | 758 | path = self._path.copy() 759 | self._path.clear() 760 | 761 | if self._credentials: 762 | db_ref = _build_db(self.__datastore, path) 763 | 764 | db_ref.update(data) 765 | 766 | else: 767 | req_ref = f"{self._base_url}:commit?key={self._api_key}" 768 | 769 | body = { 770 | "writes": [ 771 | Message.to_dict(pbs_for_update(f"{self._base_path}/{'/'.join(path)}", data, None)[0]) 772 | ] 773 | } 774 | 775 | if token: 776 | headers = {"Authorization": "Firebase " + token} 777 | response = self._requests.post(req_ref, headers=headers, json=body) 778 | 779 | else: 780 | response = self._requests.post(req_ref, json=body) 781 | 782 | raise_detailed_error(response) 783 | 784 | 785 | def _build_db(db, path): 786 | """ Returns a reference to Collection/Document with admin 787 | credentials. 788 | 789 | 790 | :type db: :class:`~google.cloud.firestore.Client` 791 | :param db: Reference to Firestore Client. 792 | 793 | :type path: list 794 | :param path: Collective form of strings to create a document. 795 | 796 | 797 | :return: Reference to collection/document to perform CRUD 798 | operations. 799 | :rtype: :class:`~google.cloud.firestore_v1.document.CollectionReference` 800 | or :class:`~google.cloud.firestore_v1.document.DocumentReference` 801 | """ 802 | 803 | n = ceil(len(path) / 2) 804 | 805 | for _ in range(n): 806 | db = db.collection(path.pop(0)) 807 | 808 | if len(path) > 0: 809 | db = db.document(path.pop(0)) 810 | 811 | return db 812 | -------------------------------------------------------------------------------- /firebase/firestore/_utils.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from datetime import datetime 9 | from base64 import b64encode, b64decode 10 | from google.protobuf.json_format import MessageToDict 11 | from google.cloud.firestore_v1._helpers import GeoPoint 12 | from google.api_core.datetime_helpers import DatetimeWithNanoseconds 13 | 14 | 15 | def _from_datastore(data): 16 | """ Converts a map of Firestore ``data``-s to Python dictionary. 17 | 18 | 19 | :type data: dict 20 | :param data: A map of firestore data. 21 | 22 | 23 | :return: A dictionary of native Python values converted 24 | from the ``data``. 25 | :rtype: dict 26 | """ 27 | 28 | data_to_restructure = data['fields'] 29 | 30 | for key, val in data_to_restructure.items(): 31 | 32 | if isinstance(val.get('mapValue'), dict): 33 | 34 | if val['mapValue'].get('fields', False): 35 | data_to_restructure[key] = _from_datastore(val['mapValue']) 36 | else: 37 | data_to_restructure[key] = {} 38 | 39 | elif isinstance(val.get('arrayValue'), dict): 40 | arr = [] 41 | 42 | if val['arrayValue'].get('values'): 43 | for x in val['arrayValue']['values']: 44 | arr.append(_decode_datastore(x)) 45 | 46 | data_to_restructure[key] = arr 47 | 48 | else: 49 | data_to_restructure[key] = _decode_datastore(val) 50 | 51 | return data_to_restructure 52 | 53 | 54 | def _decode_datastore(value): 55 | """ Converts a Firestore ``value`` to a native Python value. 56 | 57 | 58 | :type value: dict 59 | :param value: A Firestore data to be decoded / parsed / 60 | converted. 61 | 62 | 63 | :return: A native Python value converted from the ``value``. 64 | :rtype: :data:`None` or :class:`bool` or :class:`bytes` 65 | or :class:`int` or :class:`float` or :class:`str` or 66 | :class:`dict` or 67 | :class:`~google.api_core.datetime_helpers.DatetimeWithNanoseconds` 68 | or :class:`~google.cloud.firestore_v1._helpers.GeoPoint`. 69 | 70 | :raises TypeError: For value types that are unsupported. 71 | """ 72 | 73 | if value.get('nullValue', False) is None: 74 | return value['nullValue'] 75 | 76 | elif value.get('booleanValue') is not None: 77 | return bool(value['booleanValue']) 78 | 79 | elif value.get('bytesValue'): 80 | return b64decode(value['bytesValue'].encode('utf-8')) 81 | 82 | elif value.get('integerValue'): 83 | return int(value['integerValue']) 84 | 85 | elif value.get('doubleValue'): 86 | return float(value['doubleValue']) 87 | 88 | elif isinstance(value.get('stringValue'), str): 89 | return str(value['stringValue']) 90 | 91 | elif value.get('mapValue'): 92 | return _from_datastore(value['mapValue']) 93 | 94 | elif value.get('timestampValue'): 95 | return DatetimeWithNanoseconds.from_rfc3339(value['timestampValue']) 96 | 97 | elif value.get('geoPointValue'): 98 | return GeoPoint(float(value['timestampValue']['latitude']), float(value['timestampValue']['longitude'])) 99 | 100 | else: 101 | raise TypeError("Cannot convert to a Python Value", value, "Invalid type", type(value)) 102 | 103 | 104 | def _to_datastore(data): 105 | """ Converts a Python dictionary ``data``-s to map of Firestore. 106 | 107 | 108 | :type data: dict 109 | :param data: A Python dictionary containing data. 110 | 111 | 112 | :return: A map of Firebase values converted from the ``data``. 113 | :rtype: dict 114 | 115 | :raises ValueError: Raised when a key in the python dictionary is 116 | Non-alphanum char without *`* (ticks) at start and end. 117 | """ 118 | 119 | restructured_data = {} 120 | 121 | for key, val in data.items(): 122 | 123 | if ' ' in key and (not key.startswith('`') or not key.endswith('`')): 124 | raise ValueError(f'Non-alphanum char in element with leading alpha: {key}') 125 | 126 | key = str(key) 127 | 128 | if isinstance(val, dict): 129 | restructured_data[key] = {'mapValue': _to_datastore(val)} 130 | 131 | elif isinstance(val, list): 132 | arr = [] 133 | 134 | for x in val: 135 | arr.append(_encode_datastore_value(x)) 136 | 137 | restructured_data[key] = {'arrayValue': {'values': arr}} 138 | 139 | else: 140 | restructured_data[key] = _encode_datastore_value(val) 141 | 142 | return {'fields': restructured_data} 143 | 144 | 145 | def _encode_datastore_value(value): 146 | """ Converts a Python ``value`` to a Firebase value. 147 | 148 | 149 | :type value: :data:`None` or :class:`bool` or :class:`bytes` 150 | or :class:`int` or :class:`float` or :class:`str` or 151 | :class:`dict` or :class:`~datetime.datetime` or 152 | :class:`~google.api_core.datetime_helpers.DatetimeWithNanoseconds` 153 | or :class:`~google.cloud.firestore_v1._helpers.GeoPoint`. 154 | :param value: A Python data to be encoded/converted to Firebase. 155 | 156 | 157 | :return: A Firebase value converted from ``value``. 158 | :rtype: dict 159 | 160 | :raises TypeError: Raised when unsupported data type given. 161 | """ 162 | 163 | if value is None: 164 | return {'nullValue': value} 165 | 166 | elif isinstance(value, bytes): 167 | return {'bytesValue': b64encode(value).decode('utf-8')} 168 | 169 | elif isinstance(value, bool): 170 | return {'booleanValue': value} 171 | 172 | elif isinstance(value, int): 173 | return {'integerValue': value} 174 | 175 | elif isinstance(value, float): 176 | return {'doubleValue': value} 177 | 178 | elif isinstance(value, str): 179 | return {'stringValue': value} 180 | 181 | elif isinstance(value, dict): 182 | return {'mapValue': _to_datastore(value)} 183 | 184 | elif isinstance(value, datetime): 185 | return {'timestampValue': value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")} 186 | 187 | elif isinstance(value, DatetimeWithNanoseconds): 188 | return {'timestampValue': value.rfc3339()} 189 | 190 | elif isinstance(value, GeoPoint): 191 | return {'geoPointValue': MessageToDict(value.to_protobuf())} 192 | else: 193 | 194 | raise TypeError("Cannot convert to a Firestore Value", value, "Invalid type", type(value)) 195 | -------------------------------------------------------------------------------- /firebase/storage/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | """ 9 | A simple python wrapper for Google's `Firebase Cloud Storage REST API`_ 10 | 11 | .. _Firebase Cloud Storage REST API: 12 | https://firebase.google.com/docs/reference/rest/storage/rest 13 | """ 14 | 15 | import datetime 16 | from google.cloud import storage 17 | from urllib.parse import quote 18 | 19 | from firebase._exception import raise_detailed_error 20 | 21 | 22 | class Storage: 23 | """ Firebase Cloud Storage Service 24 | 25 | :type credentials: 26 | :class:`~google.oauth2.service_account.Credentials` 27 | :param credentials: Service Account Credentials. 28 | 29 | :type requests: :class:`~requests.Session` 30 | :param requests: Session to make HTTP requests. 31 | 32 | :type storage_bucket: str 33 | :param storage_bucket: ``storageBucket`` from Firebase 34 | configuration. 35 | """ 36 | 37 | def __init__(self, credentials, requests, storage_bucket): 38 | """ Constructor """ 39 | 40 | self.credentials = credentials 41 | self.requests = requests 42 | self.storage_bucket = "https://firebasestorage.googleapis.com/v0/b/" + storage_bucket 43 | 44 | self.path = "" 45 | 46 | if credentials: 47 | client = storage.Client(credentials=credentials, project=storage_bucket) 48 | self.bucket = client.get_bucket(storage_bucket) 49 | 50 | def child(self, *args): 51 | """ Build paths to your storage. 52 | 53 | 54 | :type args: str 55 | :param args: Positional arguments to build path to storage. 56 | 57 | 58 | :return: A reference to the instance object. 59 | :rtype: Storage 60 | """ 61 | 62 | new_path = "/".join(args) 63 | 64 | if self.path: 65 | self.path += "/{}".format(new_path) 66 | else: 67 | if new_path.startswith("/"): 68 | new_path = new_path[1:] 69 | 70 | self.path = new_path 71 | 72 | return self 73 | 74 | def put(self, file, token=None): 75 | """ Upload file to storage. 76 | 77 | | For more details: 78 | | |upload_files|_ 79 | 80 | .. |upload_files| replace:: 81 | Firebase Documentation | Upload files with Cloud Storage on 82 | Web 83 | 84 | .. _upload_files: 85 | https://firebase.google.com/docs/storage/web/upload-files#upload_files 86 | 87 | 88 | :type file: str 89 | :param file: Local path to file to upload. 90 | 91 | :type token: str 92 | :param token: (Optional) Firebase Auth User ID Token, defaults 93 | to :data:`None`. 94 | 95 | 96 | :return: Successful attempt returns :data:`None`. 97 | :rtype: :data:`None` 98 | """ 99 | 100 | # reset path 101 | path = self.path 102 | self.path = None 103 | 104 | if isinstance(file, str): 105 | file_object = open(file, 'rb') 106 | else: 107 | file_object = file 108 | 109 | request_ref = self.storage_bucket + "/o?name={0}".format(path) 110 | 111 | if token: 112 | headers = {"Authorization": "Firebase " + token} 113 | request_object = self.requests.post(request_ref, headers=headers, data=file_object) 114 | 115 | raise_detailed_error(request_object) 116 | 117 | return request_object.json() 118 | 119 | elif self.credentials: 120 | blob = self.bucket.blob(path) 121 | 122 | if isinstance(file, str): 123 | return blob.upload_from_filename(filename=file) 124 | else: 125 | return blob.upload_from_file(file_obj=file) 126 | 127 | else: 128 | request_object = self.requests.post(request_ref, data=file_object) 129 | 130 | raise_detailed_error(request_object) 131 | 132 | return request_object.json() 133 | 134 | def delete(self, token=None): 135 | """ Delete file from storage. 136 | 137 | | For more details: 138 | | |delete_a_file|_ 139 | 140 | .. |delete_a_file| replace:: 141 | Firebase Documentation | Delete files with Cloud Storage on 142 | Web 143 | 144 | .. _delete_a_file: 145 | https://firebase.google.com/docs/storage/web/delete-files#delete_a_file 146 | 147 | 148 | :type token: str 149 | :param token: (Optional) Firebase Auth User ID Token, defaults 150 | to :data:`None`. 151 | """ 152 | 153 | # reset path 154 | path = self.path 155 | self.path = None 156 | 157 | # remove leading backlash 158 | if path.startswith('/'): 159 | path = path[1:] 160 | 161 | if self.credentials: 162 | self.bucket.delete_blob(path) 163 | else: 164 | request_ref = self.storage_bucket + "/o?name={0}".format(path) 165 | 166 | if token: 167 | headers = {"Authorization": "Firebase " + token} 168 | request_object = self.requests.delete(request_ref, headers=headers) 169 | else: 170 | request_object = self.requests.delete(request_ref) 171 | 172 | raise_detailed_error(request_object) 173 | 174 | def download(self, filename, token=None): 175 | """ Download file from storage. 176 | 177 | | For more details: 178 | | |download_data_via_url|_ 179 | 180 | .. |download_data_via_url| replace:: 181 | Firebase Documentation | Download files with Cloud Storage 182 | on Web 183 | 184 | .. _download_data_via_url: 185 | https://firebase.google.com/docs/storage/web/download-files#download_data_via_url 186 | 187 | 188 | :type filename: str 189 | :param filename: File name to be downloaded as. 190 | 191 | :type token: str 192 | :param token: (Optional) Firebase Auth User ID Token, defaults 193 | to :data:`None`. 194 | """ 195 | 196 | if self.credentials: 197 | 198 | # reset path 199 | path = self.path 200 | self.path = None 201 | 202 | # remove leading backlash 203 | if path.startswith('/'): 204 | path = path[1:] 205 | 206 | blob = self.bucket.get_blob(path) 207 | if blob is not None: 208 | blob.download_to_filename(filename) 209 | 210 | elif token: 211 | headers = {"Authorization": "Firebase " + token} 212 | r = self.requests.get(self.get_url(token), stream=True, headers=headers) 213 | 214 | if r.status_code == 200: 215 | with open(filename, 'wb') as f: 216 | for chunk in r: 217 | f.write(chunk) 218 | 219 | else: 220 | r = self.requests.get(self.get_url(token), stream=True) 221 | 222 | if r.status_code == 200: 223 | with open(filename, 'wb') as f: 224 | for chunk in r: 225 | f.write(chunk) 226 | 227 | def get_url(self, token=None, expiration_hour=24): 228 | """ Fetches URL for file. 229 | 230 | 231 | :type token: str 232 | :param token: (Optional) Firebase Auth User ID Token, defaults 233 | to :data:`None`. 234 | 235 | :type expiration_hour: int 236 | :param expiration_hour: (Optional) time in ``hour`` for URL to 237 | expire after, defaults to 24 hours. Works only for links 238 | generated with admin credentials. 239 | 240 | :return: URL for the file. 241 | :rtype: str 242 | """ 243 | 244 | # reset path 245 | path = self.path 246 | self.path = None 247 | 248 | # remove leading backlash 249 | if path.startswith('/'): 250 | path = path[1:] 251 | 252 | if self.credentials: 253 | blob = self.bucket.get_blob(path) 254 | if blob: 255 | return blob.generate_signed_url(datetime.timedelta(hours=expiration_hour), method='GET') 256 | 257 | elif token: 258 | 259 | # retrieve download tokens first 260 | headers = {"Authorization": "Firebase " + token} 261 | request_ref = "{0}/o/{1}".format(self.storage_bucket, quote(path, safe='')) 262 | request_object = self.requests.get(request_ref, headers=headers) 263 | 264 | raise_detailed_error(request_object) 265 | 266 | return "{0}/o/{1}?alt=media&token={2}".format(self.storage_bucket, quote(path, safe=''), request_object.json()['downloadTokens']) 267 | 268 | return "{0}/o/{1}?alt=media".format(self.storage_bucket, quote(path, safe='')) 269 | 270 | def list_files(self): 271 | """ List of all files in storage. 272 | 273 | | for more details: 274 | | |list_all_files|_ 275 | 276 | .. |list_all_files| replace:: 277 | Firebase Documentation | List files with Cloud Storage on 278 | Web 279 | 280 | .. _list_all_files: 281 | https://firebase.google.com/docs/storage/web/list-files#list_all_files 282 | 283 | 284 | :return: list of :class:`~gcloud.storage.blob.Blob` 285 | :rtype: :class:`~gcloud.storage.bucket._BlobIterator` 286 | """ 287 | 288 | return self.bucket.list_blobs() 289 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['flit_core >=3.2,<4'] 3 | build-backend = 'flit_core.buildapi' 4 | 5 | [project] 6 | name = "firebase-rest-api" 7 | version = "1.11.0" 8 | readme = "README.md" 9 | description = "A simple python wrapper for Google's Firebase REST API's." 10 | requires-python = ">=3.6" 11 | keywords = [ 12 | "firebase", 13 | "firebase-auth", 14 | "firebase-database", 15 | "firebase-firestore", 16 | "firebase-realtime-database", 17 | "firebase-storage", 18 | ] 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: MIT License", 23 | "Natural Language :: English", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.6", 28 | "Programming Language :: Python :: 3.7", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | ] 34 | dependencies = [ 35 | "google-auth>=2.9.1", 36 | "google-cloud-firestore>=2.5.3", 37 | "google-cloud-storage>=2.0.0", 38 | "pkce>=1.0.0", 39 | "python_jwt>=3.3.2", 40 | "requests>=2.27.1", 41 | "six>=1.16.0" 42 | ] 43 | 44 | [[project.authors]] 45 | name = "Asif Arman Rahman" 46 | email = "asifarmanrahman@gmail.com" 47 | 48 | [project.optional-dependencies] 49 | tests = [ 50 | "flit>=3.7.1", 51 | "pytest>=7.1.2", 52 | "pytest-cov>=3.0.0", 53 | "python-decouple>=3.6" 54 | ] 55 | 56 | docs = [ 57 | "Sphinx>=5.0.2", 58 | "sphinx-rtd-theme>=1.0.0", 59 | "sphinx_design>=0.2.0", 60 | "toml>=0.10.2" 61 | ] 62 | 63 | [project.urls] 64 | Source = "https://github.com/AsifArmanRahman/firebase-rest-api" 65 | Documentation = "https://firebase-rest-api.readthedocs.io/" 66 | 67 | [tool.flit.module] 68 | name = "firebase" 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | google-auth>=2.9.1 2 | google-cloud-firestore>=2.5.3 3 | google-cloud-storage>=2.0.0 4 | pkce>=1.0.0 5 | python_jwt>=3.3.2 6 | requests>=2.27.1 7 | six>=1.16.0 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from decouple import config 9 | 10 | 11 | # get this from firebase console 12 | # go to project settings, general tab and click "Add Firebase to your web app" 13 | SIMPLE_CONFIG = { 14 | "apiKey": config('FIREBASE_API_KEY'), 15 | "authDomain": config('FIREBASE_AUTH_DOMAIN'), 16 | "databaseURL": config('FIREBASE_DATABASE_URL'), 17 | "projectId": config('FIREBASE_PROJECT_ID'), 18 | "storageBucket": config('FIREBASE_STORAGE_BUCKET'), 19 | } 20 | 21 | # get this in json file from firebase console 22 | # go to project settings, service accounts tab and click generate new private key 23 | SERVICE_ACCOUNT = { 24 | "type": 'service_account', 25 | "project_id": config('FIREBASE_SERVICE_ACCOUNT_PROJECT_ID'), 26 | "private_key_id": config('FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY_ID'), 27 | "private_key": config('FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY').replace('\\n', '\n'), 28 | "client_email": config('FIREBASE_SERVICE_ACCOUNT_CLIENT_EMAIL'), 29 | "client_id": config('FIREBASE_SERVICE_ACCOUNT_CLIENT_ID'), 30 | "auth_uri": 'https://accounts.google.com/o/oauth2/auth', 31 | "token_uri": 'https://oauth2.googleapis.com/token', 32 | "auth_provider_x509_cert_url": 'https://www.googleapis.com/oauth2/v1/certs', 33 | "client_x509_cert_url": config('FIREBASE_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL'), 34 | } 35 | 36 | 37 | SERVICE_CONFIG = dict(SIMPLE_CONFIG, serviceAccount=SERVICE_ACCOUNT) 38 | 39 | 40 | # get this json file from firebase console 41 | # go to project settings, service accounts tab and click generate new private key 42 | SERVICE_ACCOUNT_PATH = "firebase-adminsdk.json" 43 | 44 | SERVICE_CONFIG_WITH_FILE_PATH = dict(SIMPLE_CONFIG, serviceAccount=SERVICE_ACCOUNT_PATH) 45 | 46 | 47 | TEST_USER_EMAIL = config('TEST_USER_EMAIL') 48 | TEST_USER_PASSWORD = config('TEST_USER_PASSWORD') 49 | 50 | TEST_USER_EMAIL_2 = config('TEST_USER_EMAIL_2') 51 | TEST_USER_PASSWORD_2 = config('TEST_USER_PASSWORD_2') 52 | -------------------------------------------------------------------------------- /tests/config.template.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | # get this from firebase console 9 | # go to project settings, general tab and click "Add Firebase to your web app" 10 | SIMPLE_CONFIG = { 11 | "apiKey": "", 12 | "authDomain": "", 13 | "databaseURL": "", 14 | "storageBucket": "", 15 | } 16 | 17 | # get this json file from firebase console 18 | # go to project settings, service accounts tab and click generate new private key 19 | SERVICE_ACCOUNT_PATH = "firebase-adminsdk.json" 20 | 21 | SERVICE_CONFIG = dict(SIMPLE_CONFIG, serviceAccount=SERVICE_ACCOUNT_PATH) 22 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Asif Arman Rahman 2 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 3 | 4 | # -------------------------------------------------------------------------------------- 5 | 6 | 7 | import pytest 8 | 9 | from tests.tools import make_auth, make_db, make_ds, make_storage 10 | from tests.config import ( 11 | TEST_USER_EMAIL, TEST_USER_PASSWORD, 12 | TEST_USER_EMAIL_2, TEST_USER_PASSWORD_2 13 | ) 14 | 15 | 16 | @pytest.fixture(scope='session') 17 | def auth(): 18 | return make_auth() 19 | 20 | 21 | @pytest.fixture(scope='session') 22 | def auth_admin(): 23 | return make_auth(True) 24 | 25 | 26 | @pytest.fixture(scope='session') 27 | def db(): 28 | # To make it easier to test, we keep the test restricted to firebase_tests 29 | # Because of the current mutations on calls, we return it as a function. 30 | try: 31 | yield lambda: make_db(service_account=True).child('firebase_tests') 32 | finally: 33 | make_db(service_account=True).child('firebase_tests').remove() 34 | 35 | 36 | @pytest.fixture(scope='session') 37 | def email(): 38 | return TEST_USER_EMAIL 39 | 40 | @pytest.fixture(scope='session') 41 | def email_2(): 42 | return TEST_USER_EMAIL_2 43 | 44 | @pytest.fixture(scope='session') 45 | def ds(): 46 | return make_ds() 47 | 48 | 49 | @pytest.fixture(scope='session') 50 | def ds_admin(): 51 | return make_ds(True) 52 | 53 | 54 | @pytest.fixture(scope='session') 55 | def password(): 56 | return TEST_USER_PASSWORD 57 | 58 | @pytest.fixture(scope='session') 59 | def password_2(): 60 | return TEST_USER_PASSWORD_2 61 | 62 | @pytest.fixture(scope='session') 63 | def storage(): 64 | return make_storage() 65 | 66 | 67 | @pytest.fixture(scope='session') 68 | def storage_admin(): 69 | return make_storage(service_account=True) 70 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsifArmanRahman/firebase-rest-api/cf2772a6549d47d6d5ee6750e9bb77f5f9c4f679/tests/requirements.txt -------------------------------------------------------------------------------- /tests/static/test-file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Purus in mollis nunc sed. Dui sapien eget mi proin sed libero. Faucibus turpis in eu mi bibendum neque. Tortor at auctor urna nunc id cursus metus aliquam. Sit amet tellus cras adipiscing enim eu. Ornare massa eget egestas purus. Nunc sed id semper risus in hendrerit. Id diam vel quam elementum pulvinar etiam non quam lacus. Interdum posuere lorem ipsum dolor sit amet. Eu lobortis elementum nibh tellus molestie nunc non. 2 | 3 | Adipiscing bibendum est ultricies integer quis auctor elit sed. Mattis vulputate enim nulla aliquet porttitor lacus luctus accumsan. Ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Justo nec ultrices dui sapien eget mi. Sit amet massa vitae tortor. A erat nam at lectus. Vestibulum lorem sed risus ultricies tristique nulla aliquet enim tortor. Sit amet consectetur adipiscing elit. Faucibus a pellentesque sit amet porttitor eget dolor. Posuere lorem ipsum dolor sit amet consectetur. Dui accumsan sit amet nulla facilisi morbi tempus iaculis urna. Neque viverra justo nec ultrices dui. Viverra aliquet eget sit amet tellus cras adipiscing. Nunc lobortis mattis aliquam faucibus purus in massa tempor nec. Diam sit amet nisl suscipit adipiscing. Ullamcorper eget nulla facilisi etiam. Eros donec ac odio tempor orci dapibus. Massa vitae tortor condimentum lacinia. In vitae turpis massa sed elementum tempus egestas. Sit amet aliquam id diam. 4 | 5 | Orci eu lobortis elementum nibh tellus molestie nunc non. Mattis vulputate enim nulla aliquet porttitor lacus luctus accumsan. Fermentum iaculis eu non diam phasellus vestibulum lorem. Lectus mauris ultrices eros in cursus turpis. Sed odio morbi quis commodo odio aenean sed adipiscing. Tortor aliquam nulla facilisi cras fermentum odio eu feugiat pretium. Amet massa vitae tortor condimentum lacinia quis vel. Vel elit scelerisque mauris pellentesque. Sit amet est placerat in egestas. Facilisi etiam dignissim diam quis enim. 6 | 7 | In nulla posuere sollicitudin aliquam ultrices. In arcu cursus euismod quis viverra nibh. Massa massa ultricies mi quis hendrerit dolor. Eros donec ac odio tempor orci dapibus. Habitant morbi tristique senectus et netus. Sed pulvinar proin gravida hendrerit lectus a. Elementum integer enim neque volutpat. Et netus et malesuada fames ac turpis egestas maecenas. Tristique magna sit amet purus gravida. Dictum at tempor commodo ullamcorper a lacus vestibulum sed. Eleifend donec pretium vulputate sapien nec. Dolor purus non enim praesent elementum. Rutrum tellus pellentesque eu tincidunt tortor aliquam nulla facilisi. Sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae. 8 | 9 | Pharetra et ultrices neque ornare aenean euismod elementum. Nullam ac tortor vitae purus faucibus ornare. Tempor orci eu lobortis elementum. Duis at tellus at urna condimentum mattis. Nulla facilisi nullam vehicula ipsum a. Aliquet nec ullamcorper sit amet risus nullam. A condimentum vitae sapien pellentesque habitant morbi tristique. Blandit volutpat maecenas volutpat blandit aliquam etiam erat velit scelerisque. Id velit ut tortor pretium viverra suspendisse potenti nullam ac. Feugiat nisl pretium fusce id velit ut tortor pretium viverra. Cursus sit amet dictum sit amet justo donec. Ut porttitor leo a diam sollicitudin tempor id. Iaculis at erat pellentesque adipiscing commodo. Eget nunc lobortis mattis aliquam faucibus purus. Nulla facilisi morbi tempus iaculis urna id volutpat lacus laoreet. Facilisi morbi tempus iaculis urna id volutpat lacus laoreet. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate. -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | import pytest 9 | import requests.exceptions 10 | 11 | 12 | class TestAuth: 13 | 14 | user = None 15 | anonymous_user = None 16 | 17 | def test_sign_in_with_non_existing_account_email_and_password(self, auth, email, password): 18 | with pytest.raises(requests.exceptions.HTTPError) as exc_info: 19 | auth.sign_in_with_email_and_password(email, password) 20 | assert "EMAIL_NOT_FOUND" in str(exc_info.value) 21 | 22 | def test_create_user_with_email_and_password(self, auth, email, password): 23 | assert auth.create_user_with_email_and_password(email, password) 24 | 25 | def test_create_user_with_existing_email_and_password(self, auth, email, password): 26 | with pytest.raises(requests.exceptions.HTTPError) as exc_info: 27 | auth.create_user_with_email_and_password(email, password) 28 | assert "EMAIL_EXISTS" in str(exc_info.value) 29 | 30 | def test_sign_in_with_email_and_wrong_password(self, auth, email): 31 | with pytest.raises(requests.exceptions.HTTPError) as exc_info: 32 | auth.sign_in_with_email_and_password(email, 'WrongPassword123') 33 | assert "INVALID_PASSWORD" in str(exc_info.value) 34 | 35 | def test_sign_in_with_email_and_password(self, auth, email, password): 36 | user = auth.sign_in_with_email_and_password(email, password) 37 | self.__class__.user = user 38 | assert user 39 | 40 | def test_sign_in_anonymous(self, auth): 41 | user = auth.sign_in_anonymous() 42 | self.__class__.anonymous_user = user 43 | assert user 44 | 45 | def test_create_custom_token(self, auth): 46 | with pytest.raises(AttributeError): 47 | auth.create_custom_token('CreateCustomToken1') 48 | 49 | def test_create_custom_token_with_claims(self, auth): 50 | with pytest.raises(AttributeError): 51 | auth.create_custom_token('CreateCustomToken2', {'premium': True}) 52 | 53 | def test_sign_in_with_custom_token(self, auth): 54 | with pytest.raises(requests.exceptions.HTTPError): 55 | auth.sign_in_with_custom_token(None) 56 | 57 | def test_refresh(self, auth): 58 | assert auth.refresh(self.__class__.user.get('refreshToken')) 59 | 60 | def test_get_account_info(self, auth): 61 | assert auth.get_account_info(self.__class__.user.get('idToken')) 62 | 63 | def test_send_email_verification(self, auth): 64 | assert auth.send_email_verification(self.__class__.user.get('idToken')) 65 | 66 | def test_send_password_reset_email(self, auth): 67 | assert auth.send_password_reset_email(self.__class__.user.get('email')) 68 | 69 | @pytest.mark.xfail 70 | def test_verify_password_reset_code(self, auth): 71 | assert auth.verify_password_reset_code('123456', 'NewTestPassword123') 72 | 73 | 74 | def test_change_email(self, auth, email_2, password): 75 | user = auth.change_email(self.__class__.user.get('idToken'), email_2) 76 | self.__class__.user = None 77 | 78 | assert user 79 | assert self.__class__.user is None 80 | 81 | user = auth.sign_in_with_email_and_password(email_2, password) 82 | self.__class__.user = user 83 | 84 | assert user 85 | assert self.__class__.user.get('email') == email_2 86 | 87 | def test_change_password(self, auth,email_2, password_2): 88 | user = auth.change_password(self.__class__.user.get('idToken'), password_2) 89 | self.__class__.user = None 90 | 91 | assert user 92 | assert self.__class__.user is None 93 | 94 | user = auth.sign_in_with_email_and_password(email_2, password_2) 95 | self.__class__.user = user 96 | 97 | assert user 98 | 99 | def test_update_profile_display_name(self, auth): 100 | new_name = 'Test User' 101 | user = auth.update_profile(self.__class__.user.get('idToken'), display_name=new_name) 102 | assert user 103 | assert new_name == user['displayName'] 104 | 105 | def test_set_custom_user_claims(self, auth): 106 | with pytest.raises(AttributeError) as exc_info: 107 | auth.set_custom_user_claims(self.__class__.user.get('localId'), {'premium': True}) 108 | auth.set_custom_user_claims(self.__class__.anonymous_user.get('localId'), {'premium': True}) 109 | 110 | assert "'NoneType' object has no attribute 'valid'" in str(exc_info.value) 111 | 112 | def test_verify_id_token(self, auth): 113 | with pytest.raises(KeyError) as exc_info: 114 | auth.verify_id_token(self.__class__.user.get('idToken'))['premium'] is True 115 | assert "'premium'" in str(exc_info.value) 116 | 117 | with pytest.raises(KeyError) as exc_info: 118 | auth.verify_id_token(self.__class__.anonymous_user.get('idToken'))['premium'] is True 119 | assert "'premium'" in str(exc_info.value) 120 | 121 | def test_delete_user_account(self, auth): 122 | assert auth.delete_user_account(self.__class__.user.get('idToken')) 123 | assert auth.delete_user_account(self.__class__.anonymous_user.get('idToken')) 124 | 125 | 126 | class TestAuthAdmin: 127 | 128 | user = None 129 | anonymous_user = None 130 | custom_token = None 131 | custom_token_with_claims = None 132 | custom_user = None 133 | custom_user_with_claims = None 134 | 135 | def test_sign_in_with_non_existing_account_email_and_password(self, auth_admin, email, password): 136 | with pytest.raises(requests.exceptions.HTTPError) as exc_info: 137 | auth_admin.sign_in_with_email_and_password(email, password) 138 | assert "EMAIL_NOT_FOUND" in str(exc_info.value) 139 | 140 | def test_create_user_with_email_and_password(self, auth_admin, email, password): 141 | assert auth_admin.create_user_with_email_and_password(email, password) 142 | 143 | def test_create_user_with_existing_email_and_password(self, auth_admin, email, password): 144 | with pytest.raises(requests.exceptions.HTTPError) as exc_info: 145 | auth_admin.create_user_with_email_and_password(email, password) 146 | assert "EMAIL_EXISTS" in str(exc_info.value) 147 | 148 | def test_sign_in_with_email_and_wrong_password(self, auth_admin, email): 149 | with pytest.raises(requests.exceptions.HTTPError) as exc_info: 150 | auth_admin.sign_in_with_email_and_password(email, 'WrongPassword123') 151 | assert "INVALID_PASSWORD" in str(exc_info.value) 152 | 153 | def test_sign_in_with_email_and_password(self, auth_admin, email, password): 154 | user = auth_admin.sign_in_with_email_and_password(email, password) 155 | self.__class__.user = user 156 | assert user 157 | 158 | def test_sign_in_anonymous(self, auth_admin): 159 | user = auth_admin.sign_in_anonymous() 160 | self.__class__.anonymous_user = user 161 | assert user 162 | 163 | def test_create_custom_token(self, auth_admin): 164 | token = auth_admin.create_custom_token('CreateCustomToken1') 165 | self.__class__.custom_token = token 166 | assert token 167 | 168 | def test_create_custom_token_with_claims(self, auth_admin): 169 | token = auth_admin.create_custom_token('CreateCustomToken2', {'premium': True}) 170 | self.__class__.custom_token_with_claims = token 171 | assert token 172 | 173 | def test_sign_in_with_custom_token(self, auth_admin): 174 | user1 = auth_admin.sign_in_with_custom_token(self.__class__.custom_token) 175 | user2 = auth_admin.sign_in_with_custom_token(self.__class__.custom_token_with_claims) 176 | 177 | self.__class__.custom_user = user1 178 | self.__class__.custom_user_with_claims = user2 179 | 180 | assert user1 181 | assert user2 182 | 183 | def test_get_account_info(self, auth_admin): 184 | assert auth_admin.get_account_info(self.__class__.user.get('idToken')) 185 | 186 | def test_send_email_verification(self, auth_admin): 187 | assert auth_admin.send_email_verification(self.__class__.user.get('idToken')) 188 | 189 | def test_send_password_reset_email(self, auth_admin): 190 | assert auth_admin.send_password_reset_email(self.__class__.user.get('email')) 191 | 192 | @pytest.mark.xfail 193 | def test_verify_password_reset_code(self, auth_admin): 194 | assert auth_admin.verify_password_reset_code('123456', 'NewTestPassword123') 195 | 196 | def test_update_profile_display_name(self, auth_admin): 197 | new_name = 'Test User' 198 | user = auth_admin.update_profile(self.__class__.user.get('idToken'), display_name=new_name) 199 | assert user 200 | assert new_name == user['displayName'] 201 | 202 | def test_set_custom_user_claims(self, auth_admin): 203 | auth_admin.set_custom_user_claims(self.__class__.user.get('localId'), {'premium': True}) 204 | auth_admin.set_custom_user_claims(self.__class__.anonymous_user.get('localId'), {'premium': True}) 205 | 206 | def test_refresh(self, auth_admin): 207 | self.__class__.user = auth_admin.refresh(self.__class__.user.get('refreshToken')) 208 | self.__class__.custom_user = auth_admin.refresh(self.__class__.custom_user.get('refreshToken')) 209 | self.__class__.anonymous_user = auth_admin.refresh(self.__class__.anonymous_user.get('refreshToken')) 210 | 211 | def test_verify_id_token(self, auth_admin): 212 | assert auth_admin.verify_id_token(self.__class__.user.get('idToken'))['premium'] is True 213 | assert auth_admin.verify_id_token(self.__class__.anonymous_user.get('idToken'))['premium'] is True 214 | assert auth_admin.verify_id_token(self.__class__.custom_user_with_claims.get('idToken'))['premium'] is True 215 | 216 | with pytest.raises(KeyError) as exc_info: 217 | auth_admin.verify_id_token(self.__class__.custom_user.get('idToken'))['premium'] is True 218 | assert "'premium'" in str(exc_info.value) 219 | 220 | def test_delete_user_account(self, auth_admin): 221 | assert auth_admin.delete_user_account(self.__class__.user.get('idToken')) 222 | assert auth_admin.delete_user_account(self.__class__.anonymous_user.get('idToken')) 223 | assert auth_admin.delete_user_account(self.__class__.custom_user.get('idToken')) 224 | assert auth_admin.delete_user_account(self.__class__.custom_user_with_claims.get('idToken')) 225 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | import time 9 | import random 10 | import pytest 11 | import datetime 12 | from contextlib import contextmanager 13 | 14 | 15 | @pytest.fixture(scope='function') 16 | def db_sa(db): 17 | # To make it easier to test, we keep the test restricted to firebase_tests 18 | # Because of the current mutations on calls, we return it as a function. 19 | name = 'test_%05d' % random.randint(0, 99999) 20 | yield lambda: db().child(name) 21 | 22 | 23 | @contextmanager 24 | def make_stream(db, cbk): 25 | s = db.stream(cbk) 26 | try: 27 | yield s 28 | finally: 29 | s.close() 30 | 31 | 32 | @contextmanager 33 | def make_append_stream(db): 34 | l = [] 35 | 36 | def cbk(event): 37 | l.append(event) 38 | 39 | with make_stream(db, cbk) as s: 40 | yield s, l 41 | 42 | 43 | class TestSimpleGetAndPut: 44 | def test_simple_get(self, db_sa): 45 | assert db_sa().get().val() is None 46 | 47 | def test_put_succeed(self, db_sa): 48 | assert db_sa().set(True) 49 | 50 | def test_put_then_get_keeps_value(self, db_sa): 51 | db_sa().set("some_value") 52 | assert db_sa().get().val() == "some_value" 53 | 54 | def test_put_dictionary(self, db_sa): 55 | v = dict(a=1, b="2", c=dict(x=3.1, y="3.2")) 56 | db_sa().set(v) 57 | 58 | assert db_sa().get().val() == v 59 | 60 | @pytest.mark.skip 61 | def test_put_deeper_dictionary(self, db_sa): 62 | v = {'1': {'11': {'111': 42}}} 63 | db_sa().set(v) 64 | 65 | # gives: assert [None, {'11': {'111': 42}}] == {'1': {'11': {'111': 42}}} 66 | assert db_sa().get().val() == v 67 | 68 | 69 | class TestJsonKwargs: 70 | 71 | def encoder(self, obj): 72 | if isinstance(obj, datetime.datetime): 73 | return { 74 | '__type__': obj.__class__.__name__, 75 | 'value': obj.timestamp(), 76 | } 77 | return obj 78 | 79 | def decoder(self, obj): 80 | if '__type__' in obj and obj['__type__'] == datetime.datetime.__name__: 81 | return datetime.datetime.fromtimestamp(obj['value']) 82 | return obj 83 | 84 | def test_put_fail(self, db_sa): 85 | v = {'some_datetime': datetime.datetime.now()} 86 | with pytest.raises(TypeError): 87 | db_sa().set(v) 88 | 89 | def test_put_succeed(self, db_sa): 90 | v = {'some_datetime': datetime.datetime.now()} 91 | assert db_sa().set(v, json_kwargs={'default': str}) 92 | 93 | def test_put_then_get_succeed(self, db_sa): 94 | v = {'another_datetime': datetime.datetime.now()} 95 | db_sa().set(v, json_kwargs={'default': self.encoder}) 96 | assert db_sa().get(json_kwargs={'object_hook': self.decoder}).val() == v 97 | 98 | 99 | class TestChildNavigation: 100 | def test_get_child_none(self, db_sa): 101 | assert db_sa().child('lorem').get().val() is None 102 | 103 | def test_get_child_after_pushing_data(self, db_sa): 104 | db_sa().set({'lorem': "a", 'ipsum': 2}) 105 | 106 | assert db_sa().child('lorem').get().val() == "a" 107 | assert db_sa().child('ipsum').get().val() == 2 108 | 109 | def test_update_child(self, db_sa): 110 | db_sa().child('child').update({'c1/c11': 1, 'c1/c12': 2, 'c2': 3}) 111 | 112 | assert db_sa().child('child').child('c1').get().val() == {'c11': 1, 'c12': 2} 113 | assert db_sa().child('child').child('c2').get().val() == 3 114 | 115 | def test_path_equivalence(self, db_sa): 116 | db_sa().set({'1': {'11': {'111': 42}}}) 117 | 118 | assert db_sa().child('1').child('11').child('111').get().val() == 42 119 | assert db_sa().child('1/11/111').get().val() == 42 120 | assert db_sa().child('1', '11', '111').get().val() == 42 121 | assert db_sa().child(1, '11', '111').get().val() == 42 122 | 123 | 124 | class TestStreaming: 125 | def test_create_stream_succeed(self, db_sa): 126 | with make_append_stream(db_sa()) as (stream, l): 127 | time.sleep(2) 128 | assert stream is not None 129 | 130 | def test_does_initial_call(self, db_sa): 131 | with make_append_stream(db_sa()) as (stream, l): 132 | time.sleep(2) 133 | assert len(l) == 1 134 | 135 | def test_responds_to_update_calls(self, db_sa): 136 | with make_append_stream(db_sa()) as (stream, l): 137 | testdata = {"1": "a", "1_2": "b"} 138 | db_sa().set(testdata) 139 | db_sa().update({"2": "c"}) 140 | db_sa().push("3") 141 | 142 | time.sleep(2) 143 | 144 | assert len(l) > 0 145 | 146 | # a race condition from an earlier test causes an object with no data put to be first in the list sometimes 147 | if l[0]["data"] == None: 148 | assert len(l) == 4 149 | assert l[1]["data"] == testdata 150 | else: 151 | assert len(l) == 3 152 | assert l[0]["data"] == testdata 153 | 154 | 155 | class TestConditionalRequest: 156 | def test_conditional_set_succeed(self, db_sa): 157 | etag = db_sa().get_etag() 158 | result = db_sa().conditional_set({'1': 'a'}, etag) 159 | 160 | assert db_sa().child('1').get().val() == 'a' 161 | 162 | def test_conditional_set_fail(self, db_sa): 163 | etag = '{}123'.format(db_sa().get_etag()) 164 | result = db_sa().conditional_set({'2': 'b'}, etag) 165 | 166 | assert 'ETag' in result 167 | 168 | def test_conditional_remove_succeed(self, db_sa): 169 | etag = db_sa().child('1').get_etag() 170 | result = db_sa().child('1').conditional_remove(etag) 171 | 172 | assert db_sa().child('1').get().val() is None 173 | 174 | def test_conditional_remove_fail(self, db_sa): 175 | etag = '{}123'.format(db_sa().get_etag()) 176 | result = db_sa().conditional_remove(etag) 177 | 178 | assert 'ETag' in result 179 | -------------------------------------------------------------------------------- /tests/test_firestore.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | class TestFirestoreAdmin: 9 | movies1 = { 10 | 'name': 'Iron Man', 11 | 'lead': {'name': 'Robert Downey Jr.'}, 12 | 'director': '', 13 | 'released': False, 14 | 'year': 2008, 15 | 'rating': 7.9, 16 | 'prequel': None, 17 | 'cast': ['Jon Favreau', 'Gwyneth Paltrow', 'Jeff Bridges', b'J.A.R.V.I.S', 'Terrence Howard'] 18 | } 19 | 20 | movies2 = { 21 | 'name': 'Thor', 22 | 'lead': {'name': 'Chris Hemsworth'}, 23 | 'released': False, 24 | 'year': 2011, 25 | 'rating': 7.0, 26 | 'cast': ['Tom Hiddleston', 'Natalie Portman', 'Anthony Hopkins', 'Jeremy Renner', 'Stellan Skarsgård', 'Idris Elba', 'Kat Dennings'] 27 | } 28 | 29 | auto_doc_id = None 30 | 31 | def test_manual_doc_set(self, ds_admin): 32 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document('001').set(self.__class__.movies1) is None 33 | 34 | def test_auto_doc_add(self, ds_admin): 35 | doc_id = ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').add(self.__class__.movies2) 36 | assert doc_id 37 | 38 | self.__class__.auto_doc_id = doc_id 39 | 40 | def test_manual_doc_get(self, ds_admin): 41 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document('001').get() == self.__class__.movies1 42 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document(self.__class__.auto_doc_id).get() == self.__class__.movies2 43 | 44 | def test_collection_get(self, ds_admin): 45 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').get() == [{'001': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 46 | 47 | def test_collection_list_document(self, ds_admin): 48 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').list_of_documents() == ['001', self.__class__.auto_doc_id] 49 | 50 | def test_collection_get_start_after(self, ds_admin): 51 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('rating').start_after({'rating': 7.4}).get() == [{'001': self.__class__.movies1}] 52 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('rating').start_after({'rating': 6.9}).get() == [{self.__class__.auto_doc_id: self.__class__.movies2}, {'001': self.__class__.movies1}] 53 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('rating').start_after({'rating': 8.5}).get() == [] 54 | 55 | def test_collection_get_start_at(self, ds_admin): 56 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('rating').start_at({'rating': 7.4}).get() == [{'001': self.__class__.movies1}] 57 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('rating').start_at({'rating': 8.0}).get() == [] 58 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('rating').start_at({'rating': 7.0}).get() == [{self.__class__.auto_doc_id: self.__class__.movies2}, {'001': self.__class__.movies1}] 59 | 60 | def test_collection_get_select(self, ds_admin): 61 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').select(['lead.name', 'released']).get() == [{'001': {'lead': self.__class__.movies1['lead'], 'released': self.__class__.movies1['released']}}, {self.__class__.auto_doc_id: {'lead': self.__class__.movies2['lead'], 'released': self.__class__.movies2['released']}}] 62 | 63 | def test_collection_get_offset(self, ds_admin): 64 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('year').offset(1).get() == [{self.__class__.auto_doc_id: self.__class__.movies2}] 65 | 66 | def test_collection_get_limit_to_first(self, ds_admin): 67 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('year').limit_to_first(1).get() == [{'001': self.__class__.movies1}] 68 | 69 | def test_collection_get_limit_to_last(self, ds_admin): 70 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('year', direction='DESCENDING').limit_to_last(1).get() == [{'001': self.__class__.movies1}] 71 | 72 | def test_collection_get_end_at(self, ds_admin): 73 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('year').end_at({'year': 2010}).get() == [{'001': self.__class__.movies1}] 74 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('year').end_at({'year': 2021}).get() == [{'001': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 75 | 76 | def test_collection_get_end_before(self, ds_admin): 77 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').order_by('year').end_before({'year': 2023}).get() == [{'001': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 78 | 79 | def test_collection_get_where(self, ds_admin): 80 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').where('lead.name', 'in', ['Benedict Cumberbatch', 'Robert Downey Jr.']).get() == [{'001': self.__class__.movies1}] 81 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').where('rating', '<=', 8.0).order_by('rating', direction='DESCENDING').get() == [{'001': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 82 | 83 | def test_manual_doc_update(self, ds_admin): 84 | update_data = {'released': True} 85 | 86 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document('001').update(update_data) is None 87 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document('001').get(field_paths=['released']) == update_data 88 | 89 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document(self.__class__.auto_doc_id).update(update_data) is None 90 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document(self.__class__.auto_doc_id).get(field_paths=['released']) == update_data 91 | 92 | def test_manual_doc_get_filtered(self, ds_admin): 93 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document('001').get(field_paths=['name']) == {'name': self.__class__.movies1['name']} 94 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document(self.__class__.auto_doc_id).get(field_paths=['name']) == {'name': self.__class__.movies2['name']} 95 | 96 | def test_manual_doc_delete(self, ds_admin): 97 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document('001').delete() is None 98 | assert ds_admin.collection('Marvels').document('Movies').collection('PhaseOne').document(self.__class__.auto_doc_id).delete() is None 99 | 100 | 101 | class TestFirestoreAuth: 102 | movies1 = { 103 | 'name': 'Dr. Strange', 104 | 'lead': {'name': 'Benedict Cumberbatch'}, 105 | 'director': {}, 106 | 'released': False, 107 | 'year': 2016, 108 | 'rating': 7.5, 109 | 'prequel': None, 110 | 'cast': ['Tilda Swinton', 'Rachel McAdams', 'Mads Mikkelsen', 'Chiwetel Ejiofor', 'Benedict Wong'], 111 | 'producers': [] 112 | } 113 | 114 | movies2 = { 115 | 'name': 'Black Panther', 116 | 'lead': {'name': 'Chadwick Boseman'}, 117 | 'released': False, 118 | 'year': 2018, 119 | 'rating': 7.3, 120 | 'prequel': None, 121 | 'cast': ['Michael B. Jordan', 'Sebastian Stan', 'Letitia Wright', 'Martin Freeman', 'Winston Duke'] 122 | } 123 | 124 | user = None 125 | auto_doc_id = None 126 | 127 | def test_create_test_user(self, auth): 128 | user = auth.sign_in_anonymous() 129 | self.__class__.user = user 130 | assert user 131 | assert user.get('idToken') 132 | 133 | def test_manual_doc_set(self, ds): 134 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document('014').set(self.__class__.movies1, token=self.__class__.user.get('idToken')) is None 135 | 136 | def test_auto_doc_add(self, ds): 137 | doc_id = ds.collection('Marvels').document('Movies').collection('PhaseThree').add(self.__class__.movies2, token=self.__class__.user.get('idToken')) 138 | assert doc_id 139 | 140 | self.__class__.auto_doc_id = doc_id 141 | 142 | def test_manual_doc_get(self, ds): 143 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document('014').get(token=self.__class__.user.get('idToken')) == self.__class__.movies1 144 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document(self.__class__.auto_doc_id).get(token=self.__class__.user.get('idToken')) == self.__class__.movies2 145 | 146 | def test_collection_get(self, ds): 147 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 148 | 149 | def test_collection_list_documents(self, ds): 150 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').list_of_documents(token=self.__class__.user.get('idToken')) == ['014', self.__class__.auto_doc_id] 151 | 152 | def test_manual_doc_get_filtered(self, ds): 153 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document('014').get(field_paths=['name'], token=self.__class__.user.get('idToken')) == {'name': self.__class__.movies1['name']} 154 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document(self.__class__.auto_doc_id).get(field_paths=['name'], token=self.__class__.user.get('idToken')) == {'name': self.__class__.movies2['name']} 155 | 156 | def test_collection_get_start_after(self, ds): 157 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('rating').start_after({'rating': 7.4}).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}] 158 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('rating').start_after({'rating': 7.2}).get(token=self.__class__.user.get('idToken')) == [{self.__class__.auto_doc_id: self.__class__.movies2}, {'014': self.__class__.movies1}] 159 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('rating').start_after({'rating': 8.5}).get(token=self.__class__.user.get('idToken')) == [] 160 | 161 | def test_collection_get_start_at(self, ds): 162 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('rating').start_at({'rating': 7.4}).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}] 163 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('rating').start_at({'rating': 8.0}).get(token=self.__class__.user.get('idToken')) == [] 164 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('rating').start_at({'rating': 7.0}).get(token=self.__class__.user.get('idToken')) == [{self.__class__.auto_doc_id: self.__class__.movies2}, {'014': self.__class__.movies1}] 165 | 166 | def test_collection_get_select(self, ds): 167 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').select(['lead.name', 'released']).get(token=self.__class__.user.get('idToken')) == [{'014': {'lead': self.__class__.movies1['lead'], 'released': self.__class__.movies1['released']}}, {self.__class__.auto_doc_id: {'lead': self.__class__.movies2['lead'], 'released': self.__class__.movies2['released']}}] 168 | 169 | def test_collection_get_offset(self, ds): 170 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('year').offset(1).get(token=self.__class__.user.get('idToken')) == [{self.__class__.auto_doc_id: self.__class__.movies2}] 171 | 172 | def test_collection_get_limit_to_first(self, ds): 173 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('year').limit_to_first(1).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}] 174 | 175 | def test_collection_get_limit_to_last(self, ds): 176 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('year').limit_to_last(1).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}] 177 | 178 | def test_collection_get_end_at(self, ds): 179 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('year').end_at({'year': 2010}).get(token=self.__class__.user.get('idToken')) == [] 180 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('year').end_at({'year': 2021}).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 181 | 182 | def test_collection_get_end_before(self, ds): 183 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').order_by('year').end_before({'year': 2023}).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 184 | 185 | def test_collection_get_where(self, ds): 186 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').where('lead.name', 'in', ['Benedict Cumberbatch', 'Robert Downey Jr.']).get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}] 187 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').where('rating', '<=', 8.0).order_by('rating', direction='DESCENDING').get(token=self.__class__.user.get('idToken')) == [{'014': self.__class__.movies1}, {self.__class__.auto_doc_id: self.__class__.movies2}] 188 | 189 | def test_manual_doc_update(self, ds): 190 | update_data = {'released': True} 191 | 192 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document('014').update(update_data, token=self.__class__.user.get('idToken')) is None 193 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document('014').get(field_paths=['released'], token=self.__class__.user.get('idToken')) == update_data 194 | 195 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document(self.__class__.auto_doc_id).update(update_data, token=self.__class__.user.get('idToken')) is None 196 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document(self.__class__.auto_doc_id).get(field_paths=['released'], token=self.__class__.user.get('idToken')) == update_data 197 | 198 | def test_manual_doc_delete(self, ds): 199 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document('014').delete(self.__class__.user.get('idToken')) is None 200 | assert ds.collection('Marvels').document('Movies').collection('PhaseThree').document(self.__class__.auto_doc_id).delete(self.__class__.user.get('idToken')) is None 201 | 202 | def test_delete_test_user(self, auth): 203 | assert auth.delete_user_account(self.__class__.user.get('idToken')) 204 | 205 | 206 | class TestFirestore: 207 | series1 = { 208 | 'name': 'Loki', 209 | 'lead': {'name': 'Tom Hiddleston'}, 210 | 'released': False, 211 | 'year': 2021, 212 | 'rating': 8.2, 213 | 'prequel': None, 214 | 'cast': ['Sophia Di Martino', 'Owen Wilson', 'Jonathan Majors', 'Wunmi Mosaku', 'Gugu Mbatha-Raw'] 215 | } 216 | 217 | series2 = { 218 | 'name': 'Moon Knight', 219 | 'lead': {'name': 'Oscar Issac'}, 220 | 'released': False, 221 | 'year': 2022, 222 | 'rating': 7.4, 223 | 'prequel': None, 224 | 'cast': ['Ethan Hawke', 'May Calamawy', 'F. Murray Abraham'] 225 | } 226 | 227 | auto_doc_id = None 228 | 229 | def test_manual_doc_set(self, ds): 230 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document('003').set(self.__class__.series1) is None 231 | 232 | def test_auto_doc_add(self, ds): 233 | doc_id = ds.collection('Marvels').document('Series').collection('PhaseFour').add(self.__class__.series2) 234 | assert doc_id 235 | 236 | self.__class__.auto_doc_id = doc_id 237 | 238 | def test_manual_doc_get(self, ds): 239 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document('003').get() == self.__class__.series1 240 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document(self.__class__.auto_doc_id).get() == self.__class__.series2 241 | 242 | def test_collection_get(self, ds): 243 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').get() == [{'003': self.__class__.series1}, {self.__class__.auto_doc_id: self.__class__.series2}] 244 | 245 | def test_collection_list_documents(self, ds): 246 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').list_of_documents() == ['003', self.__class__.auto_doc_id] 247 | 248 | def test_manual_doc_get_filtered(self, ds): 249 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document('003').get(field_paths=['name']) == {'name': self.__class__.series1['name']} 250 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document(self.__class__.auto_doc_id).get(field_paths=['name']) == {'name': self.__class__.series2['name']} 251 | 252 | def test_collection_get_start_after(self, ds): 253 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('rating').start_after({'rating': 7.4}).get() == [{'003': self.__class__.series1}] 254 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('rating').start_after({'rating': 7.3}).get() == [{self.__class__.auto_doc_id: self.__class__.series2}, {'003': self.__class__.series1}] 255 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('rating').start_after({'rating': 8.5}).get() == [] 256 | 257 | def test_collection_get_start_at(self, ds): 258 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('rating').start_at({'rating': 7.4}).get() == [{self.__class__.auto_doc_id: self.__class__.series2}, {'003': self.__class__.series1}] 259 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('rating').start_at({'rating': 8.0}).get() == [{'003': self.__class__.series1}] 260 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('rating').start_at({'rating': 8.5}).get() == [] 261 | 262 | def test_collection_get_select(self, ds): 263 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').select(['lead.name', 'released']).get() == [{'003': {'lead': self.__class__.series1['lead'], 'released': self.__class__.series1['released']}}, {self.__class__.auto_doc_id: {'lead': self.__class__.series2['lead'], 'released': self.__class__.series2['released']}}] 264 | 265 | def test_collection_get_offset(self, ds): 266 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('year').offset(1).get() == [{self.__class__.auto_doc_id: self.__class__.series2}] 267 | 268 | def test_collection_get_limit_to_first(self, ds): 269 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('year').limit_to_first(1).get() == [{'003': self.__class__.series1}] 270 | 271 | def test_collection_get_limit_to_last(self, ds): 272 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('year').limit_to_last(1).get() == [{'003': self.__class__.series1}] 273 | 274 | def test_collection_get_end_at(self, ds): 275 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('year').end_at({'year': 2010}).get() == [] 276 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('year').end_at({'year': 2021}).get() == [{'003': self.__class__.series1}] 277 | 278 | def test_collection_get_end_before(self, ds): 279 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').order_by('year').end_before({'year': 2023}).get() == [{'003': self.__class__.series1}, {self.__class__.auto_doc_id: self.__class__.series2}] 280 | 281 | def test_collection_get_where(self, ds): 282 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').where('lead.name', 'in', ['Benedict Cumberbatch', 'Robert Downey Jr.']).get() == [] 283 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').where('rating', '<=', 8.0).get() == [{self.__class__.auto_doc_id: self.__class__.series2}] 284 | 285 | def test_manual_doc_update(self, ds): 286 | update_data = {'released': True} 287 | 288 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document('003').update(update_data) is None 289 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document('003').get(field_paths=['released']) == update_data 290 | 291 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document(self.__class__.auto_doc_id).update(update_data) is None 292 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document(self.__class__.auto_doc_id).get(field_paths=['released']) == update_data 293 | 294 | def test_manual_doc_delete(self, ds): 295 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document('003').delete() is None 296 | assert ds.collection('Marvels').document('Series').collection('PhaseFour').document(self.__class__.auto_doc_id).delete() is None 297 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | import pytest 9 | 10 | from tests.tools import initiate_app_with_service_account_file, make_auth, make_db, make_storage 11 | 12 | 13 | def test_initiate_app_with_service_account_file(): 14 | with pytest.raises(FileNotFoundError) as exc_info: 15 | initiate_app_with_service_account_file() 16 | assert "No such file or directory: 'firebase-adminsdk.json'" in str(exc_info.value) 17 | 18 | 19 | def test_setup_auth(): 20 | auth = make_auth() 21 | user = auth.sign_in_anonymous() 22 | 23 | assert auth.delete_user_account(user['idToken']) 24 | 25 | 26 | def test_setup_auth_admin(): 27 | auth = make_auth(True) 28 | user = auth.sign_in_anonymous() 29 | 30 | assert auth.delete_user_account(user['idToken']) 31 | 32 | 33 | def test_setup_db(): 34 | db = make_db(True) 35 | 36 | assert db.get() 37 | 38 | 39 | def test_setup_storage(): 40 | storage = make_storage() 41 | 42 | with pytest.raises(AttributeError) as exc_info: 43 | storage.list_files() 44 | assert 'bucket' in str(exc_info.value) 45 | 46 | 47 | def test_setup_storage_admin(): 48 | storage = make_storage(True) 49 | 50 | assert storage.list_files() 51 | -------------------------------------------------------------------------------- /tests/test_storage.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Asif Arman Rahman 2 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 3 | 4 | # -------------------------------------------------------------------------------------- 5 | import os.path 6 | 7 | import pytest 8 | 9 | 10 | class TestStorage: 11 | 12 | test_user = None 13 | 14 | def test_user_for_storage(self, auth): 15 | self.__class__.test_user = auth.sign_in_anonymous() 16 | assert self.__class__.test_user is not None 17 | 18 | def test_child(self, storage): 19 | assert storage.child('firebase-test-001') 20 | 21 | def test_put(self, storage): 22 | assert storage.child('uploaded-file.txt').put("tests/static/test-file.txt", self.__class__.test_user.get('idToken')) 23 | 24 | def test_get_url(self, storage): 25 | assert storage.child('firebase-test-001').child('uploaded-file.txt').get_url(self.__class__.test_user.get('idToken')) 26 | 27 | def test_download(self, storage): 28 | assert storage.child('firebase-test-001').child('uploaded-file.txt').download('tests/static/downloaded.txt', self.__class__.test_user.get('idToken')) is None 29 | assert os.path.exists('tests/static/downloaded.txt') 30 | 31 | def test_delete(self, storage): 32 | os.remove('tests/static/downloaded.txt') 33 | assert storage.child('firebase-test-001/uploaded-file.txt').delete(self.__class__.test_user.get('idToken')) is None 34 | 35 | def test_list_of_files(self, storage): 36 | with pytest.raises(AttributeError) as exc_info: 37 | storage.list_files() 38 | assert 'bucket' in str(exc_info.value) 39 | 40 | def test_clean_user(self, auth): 41 | assert auth.delete_user_account(self.__class__.test_user.get('idToken')) 42 | 43 | 44 | class TestStorageAdmin: 45 | 46 | def test_child(self, storage_admin): 47 | assert storage_admin.child('firebase-test-001') 48 | 49 | def test_put(self, storage_admin): 50 | assert storage_admin.child('uploaded-file.txt').put("tests/static/test-file.txt") is None 51 | 52 | def test_get_url(self, storage_admin): 53 | assert storage_admin.child('firebase-test-001').child('uploaded-file.txt').get_url(None) 54 | 55 | def test_download(self, storage_admin): 56 | assert storage_admin.child('firebase-test-001').child('uploaded-file.txt').download('tests/static/downloaded.txt') is None 57 | assert os.path.exists('tests/static/downloaded.txt') 58 | 59 | def test_delete(self, storage_admin): 60 | os.remove('tests/static/downloaded.txt') 61 | assert storage_admin.child('firebase-test-001/uploaded-file.txt').delete() is None 62 | 63 | def test_list_of_files(self, storage_admin): 64 | assert storage_admin.list_files() 65 | -------------------------------------------------------------------------------- /tests/tools.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2022 Asif Arman Rahman 3 | # Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE) 4 | 5 | # -------------------------------------------------------------------------------------- 6 | 7 | 8 | from tests import config 9 | from firebase import initialize_app 10 | 11 | 12 | def initiate_app_with_service_account_file(): 13 | return initialize_app(config.SERVICE_CONFIG_WITH_FILE_PATH) 14 | 15 | 16 | def make_auth(service_account=False): 17 | if service_account: 18 | c = config.SERVICE_CONFIG 19 | else: 20 | c = config.SIMPLE_CONFIG 21 | 22 | return initialize_app(c).auth() 23 | 24 | 25 | def make_db(service_account=False): 26 | if service_account: 27 | c = config.SERVICE_CONFIG 28 | else: 29 | c = config.SIMPLE_CONFIG 30 | 31 | return initialize_app(c).database() 32 | 33 | 34 | def make_ds(service_account=False): 35 | if service_account: 36 | c = config.SERVICE_CONFIG 37 | else: 38 | c = config.SIMPLE_CONFIG 39 | 40 | return initialize_app(c).firestore() 41 | 42 | 43 | def make_storage(service_account=False): 44 | if service_account: 45 | c = config.SERVICE_CONFIG 46 | else: 47 | c = config.SIMPLE_CONFIG 48 | 49 | return initialize_app(c).storage() 50 | --------------------------------------------------------------------------------