Skip to content

Commit

Permalink
Merge branch 'release/0.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
francipvb committed Feb 15, 2022
2 parents 2790bff + a3ebcca commit ff8d459
Show file tree
Hide file tree
Showing 18 changed files with 539 additions and 120 deletions.
9 changes: 9 additions & 0 deletions .github/scripts/publish.sh
@@ -0,0 +1,9 @@
#!/usr/bin/bash

poetry config repositories.destrepo "${REPO_URL}"

poetry publish \
--build \
--repository="destrepo" \
--username="__token__" \
--password="${PYPI_TOKEN}"
57 changes: 46 additions & 11 deletions .github/workflows/ci.yml
Expand Up @@ -3,7 +3,10 @@ on:
branches:
- "main"
- "develop"

push:
branches:
- "develop"
tags:
- "v*"

Expand All @@ -18,13 +21,16 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python_version }}
- run: pip install --upgrade pip poetry coveralls
- run: pip install --upgrade pip poetry
- uses: actions/checkout@v2
- run: poetry install
- run: poetry run pytest --cov=fastapi_firebase
- run: poetry run coverage lcov
- name: Upload coverage data to coveralls.io
run: coveralls --service=github
if: startsWith(github.ref, 'refs/heads/')
run: |
pip install coveralls
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: python${{ matrix.python_version }}
Expand All @@ -34,6 +40,7 @@ jobs:
name: Indicate completion to coveralls.io
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/heads/')
container:
image: python:3-slim
steps:
Expand All @@ -44,25 +51,53 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

deploy:
name: Publish package
publish_github:
name: Publish to github
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/setup-python@v2
with:
python-version: 3.9
- uses: actions/checkout@v2
- run: pip install --upgrade pip poetry
name: Install dependencies
- run: poetry install
- run: poetry build
name: Build package
- run: poetry publish
if: startsWith(github.ref, 'refs/tags/')
env:
PYPI_USERNAME: __token__
PYPI_PASSWORD: ${{ secrets.PYPI_TOKEN }}
- uses: softprops/action-gh-release@v0.1.14
if: startsWith(github.ref, 'refs/tags/')
with:
files: dist/*

test_publish_to_pypi:
environment: test_deployment
needs: test
name: Test publish to PyPI
if: startsWith(github.ref, 'refs/heads/')
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install --upgrade pip poetry==1.1.12
- uses: actions/checkout@v2
- run: bash ./.github/scripts/publish.sh
env:
PYPI_TOKEN: ${{ secrets.pipy_token }}
REPO_URL: ${{ secrets.pypi_repository }}

publish_to_pypi:
environment: Production
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install --upgrade pip poetry==1.1.12
- uses: actions/checkout@v2
- run: bash ./.github/scripts/publish.sh
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
REPO_URL: ${{ secrets.PYPI_REPOSITORY }}
4 changes: 3 additions & 1 deletion .gitignore
Expand Up @@ -156,4 +156,6 @@ cython_debug/
#.idea/

# End of https://www.toptal.com/developers/gitignore/api/python
.vscode/settings.json

# Firebase credential for the testing app
.vscode/firebase.json
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Expand Up @@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v4.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand Down
15 changes: 15 additions & 0 deletions .vscode/launch.json
@@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app"
],
"jinja": true
}
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,4 @@
{
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
Empty file added app/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions app/main.py
@@ -0,0 +1,39 @@
from fastapi import FastAPI
from fastapi.middleware import cors


def get_app():
from app.router import router
from fastapi_firebase import setup_firebase

app = FastAPI(
title="FastAPI firebase test app",
description="Just a test app to check the firebase integration works.",
)
app.add_middleware(
cors.CORSMiddleware,
allow_origins=["*"],
allow_headers=["*"],
allow_methods=["*"],
allow_credentials=True,
expose_headers=["*"],
)
setup_firebase(app, "./.vscode/firebase.json")

app.include_router(router, prefix="/firebase")

return app


app = get_app()

if __name__ == "__main__":
import uvicorn

from app.settings import settings

uvicorn.run(
app,
host=str(settings.HOST),
port=settings.PORT,
)
10 changes: 10 additions & 0 deletions app/router.py
@@ -0,0 +1,10 @@
from fastapi import APIRouter, Depends
import typing
from fastapi_firebase.auth import validate_token

router = APIRouter()


@router.get("/current-token")
def current_token(data: typing.Dict[str, typing.Any] = Depends(validate_token)):
return data
9 changes: 9 additions & 0 deletions app/settings.py
@@ -0,0 +1,9 @@
import pydantic


class AppSettings(pydantic.BaseSettings):
PORT: int = pydantic.Field(8000)
HOST: pydantic.IPvAnyAddress = pydantic.Field("0.0.0.0")


settings = AppSettings()
2 changes: 1 addition & 1 deletion fastapi_firebase/__init__.py
Expand Up @@ -6,4 +6,4 @@
"""
from .app import firebase_app, setup_firebase

__version__ = "0.1.0"
__version__ = "0.2.0"
75 changes: 18 additions & 57 deletions fastapi_firebase/app.py
Expand Up @@ -6,82 +6,42 @@
log = logging.getLogger()


class NotInitializedError(Exception):
class FastapiFirebaseException(Exception):
pass


def _initialize_app(
*,
app_name: str = None,
credentials_file: str = None,
credentials_content: typing.Dict[str, typing.Any] = None,
firebase_options: typing.Dict[str, typing.Any] = None,
) -> App:
"""Initialize the firebase application
Args:
app_name (str, optional): The app name to initialize. Defaults to None.
credentials_file (str, optional): The credentials file path to use. Defaults to None.
credentials_content (typing.Dict[str, typing.Any], optional): The credentials decoded
content. Defaults to None.
firebase_options (typing.Dict[str, typing.Any], optional): Additional firebase options.
Defaults to None.
Returns:
App: The newly initialized app.
Raises:
ValueError: If any validation fails.
"""
class NotInitializedError(FastapiFirebaseException):
pass

kwargs = {}
credential: credentials.Base = None
if credentials_content:
credential = credentials.Certificate(credentials_content)
elif credentials_file:
credential = credentials.Certificate(credentials_file)
else:
credential = credentials.ApplicationDefault()
if credential:
kwargs["credential"] = credential

if app_name:
kwargs["name"] = app_name
class InvalidCredentialsError(FastapiFirebaseException):
pass

if firebase_options:
kwargs["options"] = firebase_options

return initialize_app(**kwargs)
class CredentialsNotLoadedError(FastapiFirebaseException):
pass


def setup_firebase(
app: FastAPI,
credentials_file: str = None,
credentials_content: typing.Dict[str, typing.Any] = None,
credential: typing.Union[str, dict, credentials.Base] = None,
firebase_options: typing.Dict[str, typing.Any] = None,
):
"""Add a firebase app to the FastAPI app.
Args:
app (FastAPI): The FastAPI app to attach to
credentials_file (str, optional): The private certificate file to load. Defaults to None.
credentials_content (typing.Dict[str, typing.Any], optional): The (already decoded) private
key. Defaults to None.
firebase_options (typing.Dict[str, typing.Any], optional): Additional firebase options.
Defaults to None.
"""
_app: App = None

if isinstance(credential, (str, dict)):
try:
credential = credentials.Certificate(credential)
except ValueError as ex:
raise InvalidCredentialsError(*ex.args)
except IOError as ex:
raise CredentialsNotLoadedError(*ex.args)

@app.on_event("startup")
def _setup_app():
nonlocal _app
try:
_app = _initialize_app(
app_name=_app_name(app),
credentials_content=credentials_content,
credentials_file=credentials_file,
firebase_options=firebase_options,
)
_app = initialize_app(credential, firebase_options, _app_name(app))
except Exception as ex:
log.exception(
"Error while trying to initialize the firebase SDK with provided args.",
Expand All @@ -102,6 +62,7 @@ def _delete_app():
_app = None
except Exception as ex:
log.exception("Error while deleting the firebase app.", exc_info=ex)
raise


def firebase_app():
Expand Down
25 changes: 25 additions & 0 deletions fastapi_firebase/auth.py
@@ -0,0 +1,25 @@
import typing

import firebase_admin
import pydantic
from fastapi import Depends, Security
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBearer
from firebase_admin import auth

from .app import firebase_app
from .schemes import TokenData

token = HTTPBearer(
scheme_name="firebaseIdToken",
)


def validate_token(
token: HTTPAuthorizationCredentials = Security(token),
app: firebase_admin.App = Depends(firebase_app),
) -> typing.Dict[str, typing.Any]:
return auth.verify_id_token(token.credentials, app)


def token_info(token: typing.Dict[str, typing.Any] = Depends(validate_token)):
return pydantic.parse_obj_as(TokenData, token)
13 changes: 13 additions & 0 deletions fastapi_firebase/schemes.py
@@ -0,0 +1,13 @@
import datetime
import pydantic


class TokenData(pydantic.BaseModel):
provider_id: str
issuer: pydantic.HttpUrl = pydantic.Field(..., alias="iss")
audience: str = pydantic.Field(..., alias="aud")
auth_time: datetime.datetime
expires_at: datetime.datetime = pydantic.Field(..., alias="exp")
issued_at: datetime.datetime = pydantic.Field(..., alias="iat")
user_id: str
sub: str

0 comments on commit ff8d459

Please sign in to comment.