Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

## [0.11.0] - 2025-06-06

### Added

- Support for transactions, a big thanks to `@lukwam` for this! For more details of how
to use this, see the new section in the README.

## [0.10.0] - 2025-02-26

### Added
Expand Down Expand Up @@ -283,7 +290,8 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Update README.md
- Update .gitignore

[unreleased]: https://github.com/ioxiocom/firedantic/compare/0.10.0...HEAD
[unreleased]: https://github.com/ioxiocom/firedantic/compare/0.11.0...HEAD
[0.11.0]: https://github.com/ioxiocom/firedantic/compare/0.10.0...0.11.0
[0.10.0]: https://github.com/ioxiocom/firedantic/compare/0.9.0...0.10.0
[0.9.0]: https://github.com/ioxiocom/firedantic/compare/0.8.1...0.9.0
[0.8.1]: https://github.com/ioxiocom/firedantic/compare/0.8.0...0.8.1
Expand Down
151 changes: 151 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,157 @@ if __name__ == "__main__":
asyncio.run(main())
```

## Transactions

Firedantic has basic support for
[Firestore Transactions](https://firebase.google.com/docs/firestore/manage-data/transactions).
The following methods can be used in a transaction:

- `Model.delete(transaction=transaction)`
- `Model.find_one(transaction=transaction)`
- `Model.find(transaction=transaction)`
- `Model.get_by_doc_id(transaction=transaction)`
- `Model.get_by_id(transaction=transaction)`
- `Model.reload(transaction=transaction)`
- `Model.save(transaction=transaction)`
- `SubModel.get_by_id(transaction=transaction)`

When using transactions, note that read operations must come before write operations.

### Transaction examples

In this example (async and sync version of it below), we are updating a `City` to
increment and decrement the population of it, both using an instance method and a
standalone function. Please note that the `@async_transactional` and `@transactional`
decorators always expect the first argument of the wrapped function to be `transaction`;
i.e. you can not directly wrap an instance method that has `self` as the first argument
or a class method that has `cls` as the first argument.

#### Transaction example (async)

```python
import asyncio
from os import environ
from unittest.mock import Mock

import google.auth.credentials
from google.cloud.firestore import AsyncClient
from google.cloud.firestore_v1 import async_transactional, AsyncTransaction

from firedantic import AsyncModel, configure, get_async_transaction

# Firestore emulator must be running if using locally.
if environ.get("FIRESTORE_EMULATOR_HOST"):
client = AsyncClient(
project="firedantic-test",
credentials=Mock(spec=google.auth.credentials.Credentials),
)
else:
client = AsyncClient()

configure(client, prefix="firedantic-test-")


class City(AsyncModel):
__collection__ = "cities"
population: int

async def increment_population(self, increment: int = 1):
@async_transactional
async def _increment_population(transaction: AsyncTransaction) -> None:
await self.reload(transaction=transaction)
self.population += increment
await self.save(transaction=transaction)

t = get_async_transaction()
await _increment_population(transaction=t)


async def main():
@async_transactional
async def decrement_population(
transaction: AsyncTransaction, city: City, decrement: int = 1
):
await city.reload(transaction=transaction)
city.population = max(0, city.population - decrement)
await city.save(transaction=transaction)

c = City(id="SF", population=1)
await c.save()
await c.increment_population(increment=1)
assert c.population == 2

t = get_async_transaction()
await decrement_population(transaction=t, city=c, decrement=5)
assert c.population == 0


if __name__ == "__main__":
asyncio.run(main())
```

#### Transaction example (sync)

```python
from os import environ
from unittest.mock import Mock

import google.auth.credentials
from google.cloud.firestore import Client
from google.cloud.firestore_v1 import transactional, Transaction

from firedantic import Model, configure, get_transaction

# Firestore emulator must be running if using locally.
if environ.get("FIRESTORE_EMULATOR_HOST"):
client = Client(
project="firedantic-test",
credentials=Mock(spec=google.auth.credentials.Credentials),
)
else:
client = Client()

configure(client, prefix="firedantic-test-")


class City(Model):
__collection__ = "cities"
population: int

def increment_population(self, increment: int = 1):
@transactional
def _increment_population(transaction: Transaction) -> None:
self.reload(transaction=transaction)
self.population += increment
self.save(transaction=transaction)

t = get_transaction()
_increment_population(transaction=t)


def main():
@transactional
def decrement_population(
transaction: Transaction, city: City, decrement: int = 1
):
city.reload(transaction=transaction)
city.population = max(0, city.population - decrement)
city.save(transaction=transaction)

c = City(id="SF", population=1)
c.save()
c.increment_population(increment=1)
assert c.population == 2

t = get_transaction()
decrement_population(transaction=t, city=c, decrement=5)
assert c.population == 0


if __name__ == "__main__":
main()
```

## Development

PRs are welcome!
Expand Down
7 changes: 6 additions & 1 deletion firedantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
)
from firedantic._sync.ttl_policy import set_up_ttl_policies
from firedantic.common import collection_group_index, collection_index
from firedantic.configurations import CONFIGURATIONS, configure
from firedantic.configurations import (
CONFIGURATIONS,
configure,
get_async_transaction,
get_transaction,
)
from firedantic.exceptions import *
from firedantic.utils import get_all_subclasses
3 changes: 2 additions & 1 deletion firedantic/_async/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
async def truncate_collection(
col_ref: AsyncCollectionReference, batch_size: int = 128
) -> int:
"""Removes all documents inside a collection.
"""
Removes all documents inside a collection.

:param col_ref: A collection reference to the collection to be truncated.
:param batch_size: Batch size for listing documents.
Expand Down
Loading