├── tests ├── __init__.py ├── test_gunicorn_config.py ├── test_health.py ├── conftest.py └── test_items.py ├── .python-version ├── firestorefastapi ├── daos │ ├── __init__.py │ └── item.py ├── routers │ ├── __init__.py │ ├── health.py │ └── item.py ├── schemas │ ├── __item__.py │ ├── health.py │ └── item.py ├── services │ ├── __init__.py │ └── item.py ├── __init__.py ├── gunicorn_config.py ├── database.py ├── logger.py └── main.py ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── test.yaml └── pull_request_template.md ├── mypy.ini ├── scripts ├── run-uvicorn.sh ├── test-cov-html.sh ├── run-gunicorn.sh ├── docker-run.sh ├── docker-push.sh ├── docker-build.sh ├── install.sh ├── lint.sh ├── test.sh └── format.sh ├── docs ├── cloudrun-firestore-deployment.md ├── README.md └── CODE_OF_CONDUCT.md ├── .flake8 ├── .coveragerc ├── .editorconfig ├── Dockerfile ├── README.md ├── pyproject.toml ├── LICENSE ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── .gitignore └── .dockerignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.6 2 | -------------------------------------------------------------------------------- /firestorefastapi/daos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firestorefastapi/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firestorefastapi/schemas/__item__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firestorefastapi/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: anthonycorletti 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | disallow_untyped_defs = True 4 | -------------------------------------------------------------------------------- /scripts/run-uvicorn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | uvicorn firestorefastapi.main:api --reload 4 | -------------------------------------------------------------------------------- /scripts/test-cov-html.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | ./scripts/test.sh --cov-report=html ${@} 4 | -------------------------------------------------------------------------------- /docs/cloudrun-firestore-deployment.md: -------------------------------------------------------------------------------- 1 | # CloudRun, Firestore Deployment 2 | 3 | Coming soon! 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203,D10,D415 4 | docstring-convention = google 5 | -------------------------------------------------------------------------------- /scripts/run-gunicorn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | gunicorn firestorefastapi.main:api -c firestorefastapi/gunicorn_config.py 4 | -------------------------------------------------------------------------------- /scripts/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | docker run -it \ 4 | -p 8080:8080 \ 5 | anthonycorletti/firestorefastapi:latest 6 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | firestorefastapi 4 | tests 5 | 6 | omit = 7 | *__init__.py 8 | 9 | parallel = True 10 | -------------------------------------------------------------------------------- /scripts/docker-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | IMAGE_VERSION=${IMAGE_VERSION:=latest} 4 | docker push "anthonycorletti/firestorefastapi:${IMAGE_VERSION}" 5 | -------------------------------------------------------------------------------- /scripts/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | IMAGE_VERSION=${IMAGE_VERSION:=latest} 4 | docker build -t "anthonycorletti/firestorefastapi:${IMAGE_VERSION}" . 5 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | pip install --upgrade pip 4 | pip install flit 5 | 6 | flit install --deps=all --extras=all --symlink 7 | pre-commit install 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /firestorefastapi/__init__.py: -------------------------------------------------------------------------------- 1 | """firestorefastapi""" 2 | import os 3 | 4 | __version__ = os.getenv("API_TAG_VERSION", "0.1.0") 5 | __project_id__ = os.getenv("PROJECT_ID", "local") 6 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | mypy firestorefastapi 4 | flake8 firestorefastapi tests 5 | black firestorefastapi tests --check 6 | isort firestorefastapi tests scripts --check-only 7 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | ./scripts/lint.sh 4 | 5 | pytest --cov=firestorefastapi --cov=tests --cov-report=term-missing --cov-report=xml -o console_output_style=progress --disable-warnings --cov-fail-under=100 ${@} 6 | -------------------------------------------------------------------------------- /firestorefastapi/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | """gunicorn server configuration.""" 2 | import os 3 | 4 | bind = f":{os.environ.get('PORT', '8080')}" 5 | threads = 2 6 | workers = 5 7 | timeout = 60 8 | worker_class = "uvicorn.workers.UvicornWorker" 9 | -------------------------------------------------------------------------------- /firestorefastapi/schemas/health.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import BaseModel 4 | from pydantic.types import StrictStr 5 | 6 | 7 | class HealthcheckResponse(BaseModel): 8 | message: StrictStr 9 | version: StrictStr 10 | time: datetime 11 | -------------------------------------------------------------------------------- /tests/test_gunicorn_config.py: -------------------------------------------------------------------------------- 1 | from starlette.testclient import TestClient 2 | 3 | import firestorefastapi.gunicorn_config as gunicorn_config 4 | 5 | 6 | def test_gunicorn_config(client: TestClient) -> None: 7 | assert gunicorn_config.worker_class == "uvicorn.workers.UvicornWorker" 8 | -------------------------------------------------------------------------------- /tests/test_health.py: -------------------------------------------------------------------------------- 1 | from starlette.testclient import TestClient 2 | 3 | 4 | def test_healthcheck(client: TestClient): 5 | request = client.get("/healthcheck") 6 | response = request.json() 7 | assert response.get("message") == "healthy" 8 | assert response.get("version") 9 | assert response.get("time") 10 | -------------------------------------------------------------------------------- /firestorefastapi/database.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from google.cloud import firestore 4 | 5 | # Project ID is determined by the GCLOUD_PROJECT environment variable 6 | if "pytest" in sys.argv[0]: 7 | # testing db 8 | from mockfirestore import MockFirestore 9 | 10 | db = MockFirestore() 11 | else: 12 | # not a testing db 13 | db = firestore.Client() # pragma: no cover 14 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | # Sort imports one per line, so autoflake can remove unused imports 4 | isort --force-single-line-imports firestorefastapi tests scripts 5 | 6 | autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place firestorefastapi tests scripts --exclude=__init__.py 7 | black firestorefastapi tests scripts 8 | isort firestorefastapi tests scripts 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from starlette.testclient import TestClient 3 | 4 | from firestorefastapi.database import db 5 | from firestorefastapi.main import api 6 | 7 | 8 | @pytest.fixture() 9 | def client(): 10 | with TestClient(api) as client: 11 | yield client 12 | 13 | 14 | @pytest.fixture(scope="module", autouse=True) 15 | def reset_mock_firestore(): 16 | db.reset() 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6-slim 2 | 3 | WORKDIR /api 4 | COPY . /api 5 | 6 | RUN apt-get update -y \ 7 | && apt-get install build-essential -y \ 8 | && rm -rf /var/lib/apt/lists/* \ 9 | && pip install flit \ 10 | && FLIT_ROOT_INSTALL=1 flit install --deps production \ 11 | && rm -rf $(pip cache dir) 12 | 13 | CMD gunicorn firestorefastapi.main:api -c firestorefastapi/gunicorn_config.py 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | - Read the [contributing doc](../CONTRIBUTING.md) to get started! 4 | - Read the [deployment doc](./cloudrun-firestore-deployment.md) to learn how to deploy this serivce to cloud run and with firestore as your database. This repo does not contain full CICD. If you would like to have full CICD, please [contact me](https://twitter.com/anthonycorletti) for sponsored support opportunities listed [here](https://github.com/anthonycorletti/sponsor). 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firestore-fastapi [![Awesome](https://awesome.re/badge.svg)](https://github.com/steren/awesome-cloudrun/) 2 | 3 | Deploy a FastAPI service to Cloud Run that uses Google Cloud Firestore. 4 | 5 | Checkout the [contributing guide](CONTRIBUTING.md) to get started and the [docs](./docs) for more information. 6 | 7 | ### Contributions & Suggestions 8 | 9 | [Pull requests](https://github.com/anthonycorletti/cloudrun-fastapi/compare) and [issues](https://github.com/anthonycorletti/cloudrun-fastapi/issues/new) are very welcome! 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: "[QUESTION]" 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | ### First check 10 | 11 | * [ ] I used GitHub search to find a similar issue and didn't find it. 12 | * [ ] I searched the documentation via integrated search. 13 | * [ ] I already searched in Google "How to do X" and didn't find any information. 14 | 15 | ### Description 16 | 17 | How can I [...]? 18 | 19 | Is it possible to [...]? 20 | 21 | ### Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /firestorefastapi/routers/health.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from fastapi import APIRouter 4 | 5 | from firestorefastapi import __version__ 6 | from firestorefastapi.logger import logger 7 | from firestorefastapi.schemas.health import HealthcheckResponse 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.get("/healthcheck", response_model=HealthcheckResponse, tags=["health"]) 13 | def healthcheck() -> HealthcheckResponse: 14 | message = "healthy" 15 | logger.debug(message) 16 | return HealthcheckResponse( 17 | message=message, 18 | version=__version__, 19 | time=datetime.now(), 20 | ) 21 | -------------------------------------------------------------------------------- /firestorefastapi/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import socket 4 | import time 5 | 6 | 7 | def create_logger() -> logging.Logger: 8 | tz = time.strftime("%z") 9 | logging.basicConfig( 10 | format=( 11 | f"[%(asctime)s.%(msecs)03d {tz}] " 12 | f"[%(process)s] [{socket.gethostname()}] [%(pathname)s L%(lineno)d] " 13 | "[%(levelname)s] %(message)s" 14 | ), 15 | level=os.environ.get("LOGLEVEL", "INFO").upper(), 16 | datefmt="%Y-%m-%d %H:%M:%S", 17 | ) 18 | logger = logging.getLogger(__name__) 19 | return logger 20 | 21 | 22 | logger = create_logger() 23 | -------------------------------------------------------------------------------- /firestorefastapi/services/item.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from uuid import UUID 3 | 4 | from firestorefastapi.daos.item import ItemDAO 5 | from firestorefastapi.schemas.item import Item, ItemCreate, ItemUpdate 6 | 7 | item_dao = ItemDAO() 8 | 9 | 10 | class ItemService: 11 | def create_item(self, item_create: ItemCreate) -> Item: 12 | return item_dao.create(item_create) 13 | 14 | def get_item(self, id: UUID) -> Item: 15 | return item_dao.get(id) 16 | 17 | def list_items(self) -> List[Item]: 18 | return item_dao.list() 19 | 20 | def update_item(self, id: UUID, item_update: ItemUpdate) -> Item: 21 | return item_dao.update(id, item_update) 22 | 23 | def delete_item(self, id: UUID) -> None: 24 | return item_dao.delete(id) 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Make a feature suggestion 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Is your feature request related to a problem 10 | 11 | Is your feature request related to a problem? 12 | 13 | Add a clear and concise description of what the problem is. For example; I want to be able to [...] but I can't because [...] 14 | 15 | ### The solution you would like 16 | 17 | Add a clear and concise description of what you want to happen. 18 | 19 | ### Describe alternatives you have considered 20 | 21 | Add a clear and concise description of any alternative solutions or features you have considered. 22 | 23 | ### Additional context 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /firestorefastapi/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from typing import Any, Callable 4 | 5 | from fastapi import FastAPI, Request 6 | 7 | from firestorefastapi import __project_id__, __version__ 8 | from firestorefastapi.routers import health, item 9 | 10 | os.environ["TZ"] = "UTC" 11 | 12 | # 13 | # create the api 14 | # 15 | api = FastAPI(title=f"Firestore FastAPI: {__project_id__}", version=__version__) 16 | 17 | 18 | # 19 | # middleware 20 | # 21 | @api.middleware("http") 22 | async def add_process_time_header(request: Request, call_next: Callable) -> Any: 23 | start_time = time.time() 24 | response = await call_next(request) 25 | process_time = time.time() - start_time 26 | response.headers["X-Process-Time"] = str(process_time) 27 | return response 28 | 29 | 30 | # 31 | # routers 32 | # 33 | api.include_router(health.router) 34 | api.include_router(item.router) 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.9"] 16 | fail-fast: false 17 | 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: update ubuntu, install dependencies 25 | run: sudo apt-get update -y 26 | 27 | - name: set up python 28 | uses: actions/setup-python@v1 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: install dependencies 33 | run: ./scripts/install.sh 34 | 35 | - name: run tests 36 | run: ./scripts/test.sh 37 | 38 | - name: docker-build 39 | run: ./scripts/docker-build.sh 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "firestorefastapi" 7 | authors = [{name = "Anthony Corletti", email = "anthcor@gmail.com"}] 8 | readme = "README.md" 9 | classifiers = ["License :: OSI Approved :: MIT License"] 10 | dynamic = ["version", "description"] 11 | dependencies = [ 12 | "fastapi >=0.70.1", 13 | "gunicorn >=20.1.0", 14 | "uvicorn >=0.16.0", 15 | "google-cloud-firestore >=2.3.4" 16 | ] 17 | 18 | [project.optional-dependencies] 19 | test = [ 20 | "pytest >=6.2.5", 21 | "coverage >=6.1.1", 22 | "pytest-cov >=3.0.0", 23 | "mock-firestore >=0.10.0" 24 | ] 25 | dev = [ 26 | "mypy >=0.910", 27 | "flake8 >=3.9.2", 28 | "black >=21.10b0", 29 | "isort >=5.9.3", 30 | "autoflake >=1.4", 31 | "flake8-docstrings >=1.6.0", 32 | "pre-commit >=2.4.0", 33 | ] 34 | 35 | [project.urls] 36 | Home = "https://github.com/anthonycorletti/firestore-fastapi" 37 | 38 | [tool.isort] 39 | profile = "black" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Anthony Corletti 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /firestorefastapi/schemas/item.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID, uuid4 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class ItemBase(BaseModel): 8 | name: str 9 | desc: str 10 | 11 | 12 | class ItemCreate(ItemBase): 13 | id: UUID = Field(default_factory=uuid4) 14 | created_at: datetime = Field(default_factory=datetime.utcnow) 15 | updated_at: datetime = Field(default_factory=datetime.utcnow) 16 | 17 | class Config: 18 | schema_extra = { 19 | "example": { 20 | "name": "An Item", 21 | "desc": "A quite common item.", 22 | } 23 | } 24 | 25 | 26 | class ItemUpdate(ItemBase): 27 | updated_at: datetime = Field(default_factory=datetime.utcnow) 28 | 29 | class Config: 30 | schema_extra = { 31 | "example": { 32 | "name": "An Item", 33 | "desc": "A very peculiar item.", 34 | } 35 | } 36 | 37 | 38 | class Item(ItemBase): 39 | id: UUID 40 | created_at: datetime 41 | updated_at: datetime 42 | 43 | class Config: 44 | orm_mode = True 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | 12 | Write here a clear and concise description of what the bug is. 13 | 14 | ### To Reproduce 15 | 16 | Steps to reproduce the behavior with a minimum self-contained file. 17 | 18 | Replace each part with your own scenario: 19 | 20 | * Code that is causing the bug: 21 | 22 | ``` 23 | # code goes here 24 | ``` 25 | 26 | * Run this code with: 27 | 28 | ``` 29 | # command goes here 30 | ``` 31 | 32 | * It outputs: 33 | 34 | ``` 35 | output goes here 36 | ``` 37 | 38 | * But I expected it to output: 39 | 40 | ``` 41 | expected output goes here 42 | ``` 43 | 44 | ### Expected behavior 45 | 46 | Add a clear and concise description of what you expected to happen. 47 | 48 | ### Screenshots 49 | 50 | If applicable, add screenshots to help explain your problem. 51 | 52 | ### Environment 53 | 54 | * OS: [e.g. Linux / Windows / macOS] 55 | * api version [e.g. 0.3.0] 56 | * python version (`python --version`) 57 | 58 | ### Additional context 59 | 60 | Add any other context about the problem here. 61 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | > **Please do not create a pull request without creating an issue first.** 2 | > 3 | > Changes need to be discussed before proceeding, pull requests submitted without linked issues may be rejected. 4 | > 5 | > Please provide enough information so that others can review your pull request. You can skip this if you're fixing a typo – it happens. 6 | > 7 | > Please remove all quoted text above and including this line before submitting your pull request. 8 | > 9 | 10 | * [ ] I have added tests to `tests` to cover my changes. 11 | * [ ] I have updated `docs/`, if necessary. 12 | * [ ] I have updated the `README.md`, if necessary. 13 | 14 | ***What existing issue does this pull request close?*** 15 | 16 | Put `closes #issue-number` in this pull request's description to auto-close the issue that this fixes. 17 | 18 | ***How are these changes tested?*** 19 | 20 | This pull request includes automated tests for the code it touches and those tests are described below. If no tests are included, reasons why must be provided below. These changes are tested with [...] 21 | 22 | ***Demonstration*** 23 | 24 | Demonstrate your contribution. For example, what are the exact commands you ran and their output, related screenshots, screen-recordings, test runs, anything that can showcase. 25 | 26 | ***Provide additional context.*** 27 | 28 | Provide as much relevant context as you like. 29 | -------------------------------------------------------------------------------- /firestorefastapi/daos/item.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from uuid import UUID 3 | 4 | from firestorefastapi.database import db 5 | from firestorefastapi.schemas.item import Item, ItemCreate, ItemUpdate 6 | 7 | 8 | class ItemDAO: 9 | collection_name = "items" 10 | 11 | def create(self, item_create: ItemCreate) -> Item: 12 | data = item_create.dict() 13 | data["id"] = str(data["id"]) 14 | doc_ref = db.collection(self.collection_name).document(str(item_create.id)) 15 | doc_ref.set(data) 16 | return self.get(item_create.id) 17 | 18 | def get(self, id: UUID) -> Item: 19 | doc_ref = db.collection(self.collection_name).document(str(id)) 20 | doc = doc_ref.get() 21 | if doc.exists: 22 | return Item(**doc.to_dict()) 23 | return 24 | 25 | def list(self) -> List[Item]: 26 | items_ref = db.collection(self.collection_name) 27 | return [ 28 | Item(**doc.get().to_dict()) 29 | for doc in items_ref.list_documents() 30 | if doc.get().to_dict() 31 | ] 32 | 33 | def update(self, id: UUID, item_update: ItemUpdate) -> Item: 34 | data = item_update.dict() 35 | doc_ref = db.collection(self.collection_name).document(str(id)) 36 | doc_ref.update(data) 37 | return self.get(id) 38 | 39 | def delete(self, id: UUID) -> None: 40 | db.collection(self.collection_name).document(str(id)).delete() 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.2.0 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-toml 9 | - id: check-yaml 10 | args: 11 | - --unsafe 12 | - id: end-of-file-fixer 13 | - id: trailing-whitespace 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v2.32.1 16 | hooks: 17 | - id: pyupgrade 18 | args: 19 | - --py3-plus 20 | - --keep-runtime-typing 21 | - repo: https://github.com/myint/autoflake 22 | rev: v1.4 23 | hooks: 24 | - id: autoflake 25 | args: 26 | - --recursive 27 | - --in-place 28 | - --remove-all-unused-imports 29 | - --remove-unused-variables 30 | - --expand-star-imports 31 | - --exclude 32 | - __init__.py 33 | - --remove-duplicate-keys 34 | - repo: https://github.com/pycqa/isort 35 | rev: 5.10.1 36 | hooks: 37 | - id: isort 38 | name: isort (python) 39 | - id: isort 40 | name: isort (cython) 41 | types: [cython] 42 | - id: isort 43 | name: isort (pyi) 44 | types: [pyi] 45 | - repo: https://github.com/psf/black 46 | rev: 22.3.0 47 | hooks: 48 | - id: black 49 | ci: 50 | autofix_commit_msg: "[pre-commit.ci] Auto-fix from pre-commit" 51 | autoupdate_commit_msg: "[pre-commit.ci] Auto-update from pre-commit" 52 | -------------------------------------------------------------------------------- /firestorefastapi/routers/item.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Body, HTTPException 4 | from pydantic.types import UUID4 5 | 6 | from firestorefastapi.schemas.item import Item, ItemCreate, ItemUpdate 7 | from firestorefastapi.services.item import ItemService 8 | 9 | router = APIRouter() 10 | item_service = ItemService() 11 | 12 | 13 | @router.post("/items", response_model=Item, tags=["item"]) 14 | def create_item(item_create: ItemCreate = Body(...)) -> Item: 15 | return item_service.create_item(item_create) 16 | 17 | 18 | @router.get("/items/{id}", response_model=Item, tags=["item"]) 19 | def get_item(id: UUID4) -> Item: 20 | item = item_service.get_item(id) 21 | if not item: 22 | raise HTTPException(status_code=404, detail="Item not found.") 23 | return item 24 | 25 | 26 | @router.get("/items", response_model=List[Item], tags=["item"]) 27 | def list_items() -> List[Item]: 28 | items = item_service.list_items() 29 | if not items: 30 | raise HTTPException(status_code=404, detail="Items not found.") 31 | return items 32 | 33 | 34 | @router.put("/items/{id}", response_model=Item, tags=["item"]) 35 | def update_item(id: UUID4, item_update: ItemUpdate = Body(...)) -> Item: 36 | item = item_service.get_item(id) 37 | if not item: 38 | raise HTTPException(status_code=404, detail="Item not found.") 39 | return item_service.update_item(id, item_update) 40 | 41 | 42 | @router.delete("/items/{id}", response_model=Item, tags=["item"]) 43 | def delete_item(id: UUID4) -> Item: 44 | item = item_service.get_item(id) 45 | if not item: 46 | raise HTTPException(status_code=404, detail="Item not found.") 47 | return item_service.delete_item(id) 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute! 2 | 3 | #### **Did you find a bug?** 4 | 5 | - **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/anthonycorletti/firestore-fastapi/issues). 6 | 7 | - If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/anthonycorletti/firestore-fastapi/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 8 | 9 | #### **Did you write a patch that fixes a bug?** 10 | 11 | - Open a new GitHub pull request with the patch. 12 | 13 | - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 14 | 15 | #### **Did you fix whitespace, format code, or make a purely cosmetic patch?** 16 | 17 | Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of this codebase and will generally not be accepted. 18 | 19 | Thanks! Read on for technical details below to get your environment ready to build. 20 | 21 | # Technical Guide 22 | 23 | Assuming you have cloned this repository to your local machine, you can follow these guidelines to make contributions. 24 | 25 | **First, please install pyenv https://github.com/pyenv/pyenv to manage your python environment.** 26 | 27 | Install the version of python as mentioned in this repo. 28 | 29 | ```sh 30 | pyenv install $(cat .python-version) 31 | ``` 32 | 33 | ## Use a virtual environment 34 | 35 | ```sh 36 | python -m venv .venv 37 | ``` 38 | 39 | This will create a directory `.venv` with python binaries and then you will be able to install packages for that isolated environment. 40 | 41 | Next, activate the environment. 42 | 43 | ```sh 44 | source .venv/bin/activate 45 | ``` 46 | 47 | To check that it worked correctly; 48 | 49 | ```sh 50 | which python pip 51 | ``` 52 | 53 | You should see paths that use the .venv/bin in your current working directory. 54 | 55 | ## Installing with Flit 56 | 57 | This project uses `flit` to manage our project's dependencies. 58 | 59 | Install dependencies, including flit. 60 | 61 | ```sh 62 | ./scripts/install.sh 63 | pyenv rehash 64 | ``` 65 | 66 | ## Formatting 67 | 68 | ```sh 69 | ./scripts/format.sh 70 | ``` 71 | 72 | ## Tests 73 | 74 | ```sh 75 | ./scripts/test.sh 76 | ``` 77 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pipenv 85 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 86 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 87 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 88 | # install all needed dependencies. 89 | #Pipfile.lock 90 | 91 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 92 | __pypackages__/ 93 | 94 | # Celery stuff 95 | celerybeat-schedule 96 | celerybeat.pid 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | /tmp 129 | /creds 130 | -------------------------------------------------------------------------------- /tests/test_items.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from starlette.testclient import TestClient 4 | 5 | from firestorefastapi.schemas.item import ItemCreate, ItemUpdate 6 | 7 | mock_item = ItemCreate.Config.schema_extra["example"] 8 | mock_item_update = ItemUpdate.Config.schema_extra["example"] 9 | 10 | 11 | def test_no_items(client: TestClient) -> None: 12 | request = client.get("/items") 13 | assert request.status_code == 404 14 | 15 | request = client.get(f"/items/{str(uuid4())}") 16 | assert request.status_code == 404 17 | 18 | request = client.put(f"/items/{str(uuid4())}", json=mock_item) 19 | assert request.status_code == 404 20 | 21 | request = client.delete(f"/items/{str(uuid4())}") 22 | assert request.status_code == 404 23 | 24 | 25 | def test_create_item(client: TestClient) -> None: 26 | request = client.post("/items", json=mock_item) 27 | assert request.status_code == 200 28 | data = request.json() 29 | assert data.get("name") == "An Item" 30 | assert data.get("created_at") 31 | assert data.get("updated_at") 32 | 33 | 34 | def test_get_item(client: TestClient) -> None: 35 | request = client.post("/items", json=mock_item) 36 | assert request.status_code == 200 37 | data = request.json() 38 | assert data.get("name") == "An Item" 39 | 40 | item_id = data.get("id") 41 | request = client.get(f"/items/{item_id}") 42 | assert request.status_code == 200 43 | data = request.json() 44 | assert data.get("name") == "An Item" 45 | 46 | 47 | def test_list_items(client: TestClient) -> None: 48 | request = client.get("/items") 49 | assert request.status_code == 200 50 | assert len(request.json()) == 2 51 | 52 | 53 | def test_update_item(client: TestClient) -> None: 54 | request = client.post("/items", json=mock_item) 55 | assert request.status_code == 200 56 | data = request.json() 57 | assert data.get("desc") == "A quite common item." 58 | 59 | item_id = data.get("id") 60 | request = client.put(f"/items/{item_id}", json=mock_item_update) 61 | assert request.status_code == 200 62 | data = request.json() 63 | assert data.get("desc") == "A very peculiar item." 64 | 65 | 66 | def test_delete_item(client: TestClient) -> None: 67 | request = client.post("/items", json=mock_item) 68 | assert request.status_code == 200 69 | data = request.json() 70 | item_id = data.get("id") 71 | 72 | request = client.delete(f"/items/{item_id}") 73 | assert request.status_code == 200 74 | 75 | request = client.get("/items") 76 | assert request.status_code == 200 77 | assert item_id not in [item.get("id") for item in request.json()] 78 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python,linux,osx,windows,vagrant 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv/ 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | 96 | ### Linux ### 97 | *~ 98 | 99 | # temporary files which can be created if a process still has a handle open of a deleted file 100 | .fuse_hidden* 101 | 102 | # KDE directory preferences 103 | .directory 104 | 105 | # Linux trash folder which might appear on any partition or disk 106 | .Trash-* 107 | 108 | # .nfs files are created when an open file is removed but is still being accessed 109 | .nfs* 110 | 111 | 112 | ### OSX ### 113 | *.DS_Store 114 | .AppleDouble 115 | .LSOverride 116 | 117 | # Icon must end with two \r 118 | Icon 119 | # Thumbnails 120 | ._* 121 | # Files that might appear in the root of a volume 122 | .DocumentRevisions-V100 123 | .fseventsd 124 | .Spotlight-V100 125 | .TemporaryItems 126 | .Trashes 127 | .VolumeIcon.icns 128 | .com.apple.timemachine.donotpresent 129 | # Directories potentially created on remote AFP share 130 | .AppleDB 131 | .AppleDesktop 132 | Network Trash Folder 133 | Temporary Items 134 | .apdisk 135 | 136 | 137 | ### Windows ### 138 | # Windows image file caches 139 | Thumbs.db 140 | ehthumbs.db 141 | 142 | # Folder config file 143 | Desktop.ini 144 | 145 | # Recycle Bin used on file shares 146 | $RECYCLE.BIN/ 147 | 148 | # Windows Installer files 149 | *.cab 150 | *.msi 151 | *.msm 152 | *.msp 153 | 154 | # Windows shortcuts 155 | *.lnk 156 | 157 | 158 | ### Vagrant ### 159 | .vagrant/ 160 | ### Local rules, see .gitignore.tail to override! ### 161 | shippable 162 | .git 163 | 164 | tmp/ -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | 77 | --------------------------------------------------------------------------------