├── .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 |
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]: "
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 |
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]: "
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 |
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]: "
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 |
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 |
9 |
10 |
15 |
16 |
32 |
33 |
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` or
7 | :ref:`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`
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`
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`.
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`
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`
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()`, :ref:`push()`,
16 | :ref:`set()`, :ref:`update()`,
17 | :ref:`remove()` and
18 | :ref:`stream()`.
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 `__
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 `__.
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" `__
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 `__.
376 |
377 | See :ref:`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`
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`
126 |
127 | ``firebaseApp.database()`` - :ref:`Database`
128 |
129 | ``firebaseApp.firestore()`` - :ref:`Firestore`
130 |
131 | ``firebaseApp.storage()`` - :ref:`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`.
61 | It is recommended to setup database before
62 | :ref:`registering an 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 `__ 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)`
31 |
32 | 2. Register an Web App.
33 | :ref:`(guide)`
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[^:]*):?( ?(?P.*))?')
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 |
--------------------------------------------------------------------------------