{"id":13533836,"url":"https://github.com/jhrcook/coffee-counter-appwrite-demo","last_synced_at":"2025-07-13T09:37:49.905Z","repository":{"id":115870028,"uuid":"418128371","full_name":"jhrcook/coffee-counter-appwrite-demo","owner":"jhrcook","description":"A demonstration of using an Appwrite backend for a Coffee Counting app.","archived":false,"fork":false,"pushed_at":"2021-10-17T19:06:29.000Z","size":2721,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-08T17:13:43.433Z","etag":null,"topics":["appwrite","demo","fastapi","pydantic","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jhrcook.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-10-17T12:44:00.000Z","updated_at":"2025-01-19T14:09:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"d137c624-db98-41e5-8fd0-9c127141bdc4","html_url":"https://github.com/jhrcook/coffee-counter-appwrite-demo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jhrcook/coffee-counter-appwrite-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhrcook%2Fcoffee-counter-appwrite-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhrcook%2Fcoffee-counter-appwrite-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhrcook%2Fcoffee-counter-appwrite-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhrcook%2Fcoffee-counter-appwrite-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jhrcook","download_url":"https://codeload.github.com/jhrcook/coffee-counter-appwrite-demo/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhrcook%2Fcoffee-counter-appwrite-demo/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265121389,"owners_count":23714503,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appwrite","demo","fastapi","pydantic","python"],"created_at":"2024-08-01T07:01:23.577Z","updated_at":"2025-07-13T09:37:49.852Z","avatar_url":"https://github.com/jhrcook.png","language":"Python","funding_links":[],"categories":["Showcase (Built with Appwrite 📣)"],"sub_categories":["Python"],"readme":"# Coffee Counter API with Appwrite backend database demo\n\n[![python](https://img.shields.io/badge/Python-3.9.6-3776AB.svg?style=flat\u0026logo=python\u0026logoColor=white)](https://www.python.org)\n[![FastAPI](https://img.shields.io/badge/FastAPI-0.79.0-009688.svg?style=flat\u0026logo=FastAPI\u0026logoColor=white)](https://fastapi.tiangolo.com)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit\u0026logoColor=white)](https://github.com/pre-commit/pre-commit)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) \u003cbr\u003e\n\u003ca href=\"https://appwrite.io/\" target=\"_blank\"\u003e\n  \u003cimg style=\"width: 160px;\" src=\"https://appwrite.io/images-ee/press/badge-pink-button.svg\" alt=\"Built with Appwrite\"\u003e\n\u003c/a\u003e\n\nThis repo serves as a demonstration of using [Appwrite's](https://appwrite.io) Database services in a Python application.\nThe demo would be part of an app that tracks coffee consumption.\nThe API is written with [FastAPI](https://fastapi.tiangolo.com) with data validation by [pydantic](https://pydantic-docs.helpmanual.io).\n\nMy current working version of this app (not using Appwrite, at the moment) can be found [here](https://github.com/jhrcook/coffee-counter-api).\n\n### Run the app\n\nFirst, you'll need to set up Appwrite.\nThis is a very easy process to do either locally or on a cloud service such as Linode or DigitalOcean.\nSee Appwrite's [docs](https://appwrite.io/docs/installation) for how to install locally or my tutorial on [how to get Appwrite up-and-running on DigitalOcean](https://joshuacook.netlify.app/post/appwrite-digital-ocean/).\nOnce this is set, create a new Project and add the two Collections described below.\nReplace the current IDs in the [configuration](config.yaml) file.\n\nWith Appwrite setup, you can run this project using the following commands.\n\n```bash\n# Setup python env and install dependencies\npython -m venv .env\nsource .env/bin/activate\npip install --upgrade pip \u0026\u0026 pip install -r requirements.txt\n\n# Start the app.\nuvicorn main:app --reload\n```\n\nThen go to [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) in your browser.\n\n---\n\n## Description\n\nThe overall point of this app is to serve as a method for tracking my coffee consumption.\nI want more than just the number of cups, but also the coffee bag that was used and the lifetime of the coffee bags.\nYou can read more about the purpose and the various tools I have built around the API on my [website](https://joshuacook.netlify.app/project/coffee-counting/).\n\nBelow, I try to describe the various pieces of the demo.\nI begin by detailing the data models followed by the API endpoints and main features of the demo app.\nNext, I describe how to use Appwrite's [Python SDK](https://github.com/appwrite/sdk-for-python) to read and write data to the Appwrite database.\nFinally, I cover how I used pydantic to act as a data validation layer between the result of a database query and the main code.\n\n### Data models\n\nThe main data models for this app are the `CoffeeBag` and `CoffeeCup` defined in [\"models.py\"](models.py).\nA coffee bag has a brand, name, mass, start and end date, roast (e.g. light, medium, dark), and indication of if it is \"active\" (i.e. in use).\nA coffee cup just has a datetime attribute to indicate when it was made and a `bag_id` to link it to a coffee bag.\nOne change that could be made would be to embed the coffee cups into an array attribute of the coffee bag, but for this demonstration, I decided to keep them separate for simplicity's sake.\nIf I were to further develop this into a full app, I would likely make this change to help with organization as the app grows in complexity.\n\n```python\nclass CoffeeBag(BaseModel):\n    \"\"\"Coffee bag data.\"\"\"\n    brand: str\n    name: str\n    mass: PositiveFloat\n    start_date: Optional[date] = None\n    end_date: Optional[date] = None\n    active: bool\n    roast: Optional[CoffeeBeanRoast] = None\n\nclass CoffeeCup(BaseModel):\n    \"\"\"Coffee cup data.\"\"\"\n    bag_id: str\n    datetime: datetime\n```\n\n### API features\n\nSince this is just a demo, I focused on the main features and API endpoints.\nAlso, I ignored the rough edges of managing datetimes (e.g. time zones and location).\nFor this demo, I have `GET` endpoints for getting all coffee bags, a specific coffee bag (by its ID), all cups of coffee, and a specific cup of coffee (by its ID).\nThere are also `PUT` endpoints for adding a bag of coffee and a new cup of coffee.\n\n![API documentation](assets/api-docs.png)\n\nOne of the nice features of the FastAPI's use of typehints is the increased details in the docuemntation.\nFor instance, in the `bags/` endpoint, there are various filter options, one of which is by the roast of the beans:\n\n```python\n@app.get(\"/bags\")\ndef coffee_bags(\n    start: Optional[date] = None,\n    end: Optional[date] = None,\n    brand: Optional[str] = None,\n    roast: Optional[CoffeeBeanRoast] = None,\n    active: Optional[bool] = None,\n) -\u003e list[CoffeeBagDocument]:\n\t...\n```\n\nBecause the roast is defined as an enumeration, the documentation automatically shows the available options:\n\n![](assets/api-roast-options.png)\n\n### Appwrite database interactions through the Python SDK\n\nIt is possible to interact with the Appwrite server through a [web API](https://appwrite.io/docs) or their [Python SDK](https://github.com/appwrite/sdk-for-python).\nFor this demonstration, I used the Python SDK, though I think it is still a bit of a work-in-progress.\nThe documentation for the Appwrite Database is available [here](https://appwrite.io/docs/client/database).\n\nThe structure of an Appwrite Database is divided into \"collections\" and \"documents\" where the former is the table and the latter individual data points or rows in the table.\n\nThe first step is to set up the two collections on the server, one for coffee bag and one for cups.\nThis can be accomplished programmatically through the SDK or web API.\nFor this size of job, it is easiest to just do it in the Appwrite GUI Console.\nThe 30 Days of Appwrite post [\"Appwrite Database\"](https://dev.to/appwrite/30daysofappwrite-appwrite-database-22an) describes how to do this easily.\nBelow are some screenshots from the console.\n\n![Appwrite console collections](assets/appwrite-collections.png)\n![Appwrite console coffee bag collection](assets/appwrite-coffee-bags.png)\n![Appwrite console coffee bag documents](assets/appwrite-coffee-bag-doc.png)\n\nThe code for interacting with the Appwrite databases is in [\"appwrite_backend.py\"](appwrite_backend.py).\nThe general flow is to create a `Client`, use the `Client` to get a `Database`, and then make queries through the `Database`.\nA simple example of this is the function to get a specific bag of coffee using its unique ID.\nMore information on the conversion to data class objects is discussed in the next section.\n\n```python\ndef get_coffee_bag(id: str) -\u003e CoffeeBagDocument:\n    db = _get_database()  # Makes a `Client` and retrieves a `Database` object\n    # Get a `document` coffee bag using its ID.\n    res = db.get_document(\n        collection_id=_get_coffee_bag_collections_id(), document_id=id\n    )\n    return CoffeeBagDocument(**res)\n```\n\nThere is also an option to get all documents in a collection.\nAn example of this is in the retrieval of all the bags of coffee with optional filters.\nThe filters are individual strings contained in a list and passed in the `filters` parameter of the query.\nNote that a boolean value must be converted to an integer (I found this out with a bit of trial-and-error).\n\n```python\ndef get_coffee_bags(\n    brand: Optional[str], active: Optional[bool], roast: Optional[CoffeeBeanRoast]\n) -\u003e list[CoffeeBagDocument]:\n    db = _get_database()\n\n    filters = []\n    if brand is not None:\n        filters.append(f\"brand={brand}\")\n    if active is not None:\n        filters.append(f\"active={int(active)}\")\n    if roast is not None:\n        filters.append(f\"roast={roast}\")\n\n    print(filters)\n    res = db.list_documents(_get_coffee_bag_collections_id(), filters=filters)\n    return [CoffeeBagDocument(**info) for info in res[\"documents\"]]\n```\n\n### Appwrite `Collections` and `Documents` with pydantic\n\nIn this app, I use [pydantic](https://pydantic-docs.helpmanual.io) to act as a data validation and standardization layer between the queries to Appwrite's database and the main code.\nThe data returned by the functions in [\"appwrite_backend.py\"](appwrite_backend.py) all return validated data class objects.\n\nAlong with the data validation, this also acts as a decoupling layer between the Appwrite-facing code and the main code.\nThus, if there are changes on the Appwrite side, the return values can remain consistent.\n\nI used a \"mixin-like\" pattern for the pydantic models.\nI began with a model that abstracts the object in question such as a coffee bag and the `CoffeeBag` class.\nThen, I created an `AppwriteDocument` class that contains the attributes common to all Appwrite Document return types: and ID, collection ID, and permission fields.\nTo put these all together, I just made a third type `CoffeeBagDocument(CoffeeBag, AppwriteDocument)` that subclassed both of these objects.\nThat way, I could use a `CoffeeBag` abstraction when appropriate, such as getting a new bag from a user, or a coffee bag Document when needed, such as the return from a database query.\n\n```python\nclass CoffeeBag(BaseModel):\n    \"\"\"Coffee bag data.\"\"\"\n\n    brand: str\n    name: str\n    mass: PositiveFloat\n    start_date: Optional[date] = None\n    end_date: Optional[date] = None\n    active: bool\n    roast: Optional[CoffeeBeanRoast] = None\n\n    def __init__(self, **data) -\u003e None:\n        for k in [\"start_date\", \"end_date\", \"roast\"]:\n            if data.get(k, None) == \"\":\n                data[k] = None\n        super().__init__(**data)\n\nclass AppwriteDocumentPermissions(BaseModel):\n    \"\"\"Appwrite document permissions.\"\"\"\n\n    read: list[str]\n    write: list[str]\n\nclass AppwriteDocument(BaseModel):\n    \"\"\"Basic fields for an Appwrite document.\"\"\"\n\n    id: str\n    collection: str\n    permissions: AppwriteDocumentPermissions\n\n    def __init__(self, **kwargs) -\u003e None:\n        for k in list(kwargs.keys()):\n            if isinstance(k, str):\n                _k = re.sub(\"^\\\\$\", \"\", k)\n                kwargs[_k] = kwargs.pop(k)\n        super().__init__(**kwargs)\n\nclass CoffeeBagDocument(CoffeeBag, AppwriteDocument):\n    \"\"\"Coffee Bag Document.\"\"\"\n```\n\nThe only annoying part of this process is that for missing data in optional fields in an Appwrite document, Appwrite returns an empty string instead of `null`.\nThis is an issue when pydantic tries to convert `\"\"` into a datetime for `start_date` or `end_date`.\nThus, I have to manually replace these missing values with `None` before pydnatic validation in the constructor of `CoffeeBag`.\n\nAnother annoyance is that the keys for the document ID, collection ID, and permission fields in the result of a database query are prepended with `\"$\"` – e.g. `id` is `$id`.\nI cannot represent these as fields in the pydantic model, so again the keys needed to be adjusted before validation in the constructor of `AppwriteDocument`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhrcook%2Fcoffee-counter-appwrite-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjhrcook%2Fcoffee-counter-appwrite-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhrcook%2Fcoffee-counter-appwrite-demo/lists"}