Skip to content
Closed
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
22 changes: 22 additions & 0 deletions .github/workflows/pr_agent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: qodo-ai/[email protected]
with:
args: '/improve --pr_code_suggestions.commitable_code_suggestions=true'
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10 changes: 6 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
name: Test
on: [push, pull_request]
on: [ push, pull_request ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10' ]
python-version: [ '3.9', '3.10', '3.11', '3.12' ]

steps:
- uses: actions/checkout@v2
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ venv.bak/
.mypy_cache/

.idea/
.qodo
37 changes: 37 additions & 0 deletions agent-coding-standards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Agent Coding Standards

# ALL CODE MUSE BE UP TO THIS STANDARD

## Follow Clean Code principles (Robert C. Martin) when writing code

- Specifically the correct order of functions

## No comments!

- Most likely there is absolutely no reason to add a comment
- If there is, it's probably a sign that the code is not clear

## Don't use dicts.

- str keys are red
- Use pydantic models instead

## Do not hallucinate

- If you are not sure, ask the user
- If you don't know the library, do the research

## Do not regress when moving code

- Make sure quality of moved code is at least as good as the original

## Don't make me ask you twice

- Follow all these rules
- Every time, all the time
- If it's hard, ask me how to solve it

## Only essential complexity, not accidental

- Use the simplest approach that works
- Question whether each line adds value or just complexity
13 changes: 8 additions & 5 deletions mockfirestore/collection.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import warnings
from typing import Any, List, Optional, Iterable, Dict, Tuple, Sequence, Union

from google.cloud.firestore_v1 import FieldFilter

from mockfirestore import AlreadyExists
from mockfirestore._helpers import generate_random_string, Store, get_by_path, set_by_path, Timestamp
from mockfirestore.query import Query
from mockfirestore.document import DocumentReference, DocumentSnapshot
from mockfirestore.query import Query


class CollectionReference:
Expand Down Expand Up @@ -41,9 +43,10 @@ def add(self, document_data: Dict, document_id: str = None) \
timestamp = Timestamp.from_now()
return timestamp, doc_ref

def where(self, field: str, op: str, value: Any) -> Query:
query = Query(self, field_filters=[(field, op, value)])
return query
def where(self, field: str = None, op: str = None, value: Any = None, *, filter: FieldFilter = None) -> Query:
if filter is not None:
return Query(self, field_filters=[(filter.field_path, filter.op_string, filter.value)])
return Query(self, field_filters=[(field, op, value)])

def order_by(self, key: str, direction: Optional[str] = None) -> Query:
query = Query(self, orders=[(key, direction)])
Expand Down Expand Up @@ -82,4 +85,4 @@ def list_documents(self, page_size: Optional[int] = None) -> Sequence[DocumentRe
def stream(self, transaction=None) -> Iterable[DocumentSnapshot]:
for key in sorted(get_by_path(self._data, self._path)):
doc_snapshot = self.document(key).get()
yield doc_snapshot
yield doc_snapshot
22 changes: 17 additions & 5 deletions mockfirestore/query.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import warnings
from itertools import islice, tee
from typing import Iterator, Any, Optional, List, Callable, Union
from typing import Any, Callable, Iterator, Optional, Union

from google.cloud.firestore_v1 import CollectionReference, FieldFilter

from mockfirestore.document import DocumentSnapshot
from mockfirestore._helpers import T
from mockfirestore.document import DocumentSnapshot


class Query:
def __init__(self, parent: 'CollectionReference', projection=None,
def __init__(self, parent: CollectionReference, projection=None,
field_filters=(), orders=(), limit=None, offset=None,
start_at=None, end_at=None, all_descendants=False) -> None:
self.parent = parent
Expand Down Expand Up @@ -61,8 +63,18 @@ def _add_field_filter(self, field: str, op: str, value: Any):
compare = self._compare_func(op)
self._field_filters.append((field, compare, value))

def where(self, field: str, op: str, value: Any) -> 'Query':
self._add_field_filter(field, op, value)
def where(
self,
field: str = None,
op: str = None,
value: Any = None,
*,
filter: FieldFilter = None,
) -> "Query":
if filter is not None:
self._add_field_filter(filter.field_path, filter.op_string, filter.value)
else:
self._add_field_filter(field, op, value)
return self

def order_by(self, key: str, direction: Optional[str] = 'ASCENDING') -> 'Query':
Expand Down
46 changes: 46 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "mock-firestore"
version = "0.12.0"
description = "A mocked version of the Google Cloud Firestore library"
authors = [{ name = "Matt Dowds" }]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = ["google-cloud-firestore"]

[project.optional-dependencies]
dev = ["google-cloud-firestore", "pytest", "black", "isort", "mypy", "flake8"]

[project.urls]
"Homepage" = "https://github.com/mdowds/mock-firestore"
"Bug Tracker" = "https://github.com/mdowds/mock-firestore/issues"

[tool.black]
line-length = 88
target-version = ["py39"]

[tool.isort]
profile = "black"
line_length = 88

[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

[dependency-groups]
dev = ["pytest>=8.3.5"]
2 changes: 1 addition & 1 deletion requirements-dev-minimal.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
google-cloud-firestore
google-cloud-firestore
6 changes: 2 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="mock-firestore",
version="0.11.0",
version="0.12.0",
author="Matt Dowds",
description="In-memory implementation of Google Cloud Firestore for use in tests",
long_description=long_description,
Expand All @@ -14,11 +14,9 @@
packages=setuptools.find_packages(),
test_suite='',
classifiers=[
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
"License :: OSI Approved :: MIT License",
],
)
139 changes: 139 additions & 0 deletions tests/test_where_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from unittest import TestCase

from google.cloud.firestore_v1 import FieldFilter

from mockfirestore import MockFirestore


class TestWhereField(TestCase):
def test_collection_whereEquals(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'valid': True},
'second': {'gumby': False}
}}

docs = list(fs.collection('foo').where(field='valid', op='==', value=True).stream())
self.assertEqual({'valid': True}, docs[0].to_dict())

def test_collection_whereEquals_with_filter(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'valid': True},
'second': {'gumby': False}
}}

docs = list(fs.collection('foo').where(filter=FieldFilter('valid', '==', True)).stream())
self.assertEqual({'valid': True}, docs[0].to_dict())

def test_collection_whereNotEquals(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'count': 1},
'second': {'count': 5}
}}

docs = list(fs.collection('foo').where('count', '!=', 1).stream())
self.assertEqual({'count': 5}, docs[0].to_dict())

def test_collection_whereLessThan(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'count': 1},
'second': {'count': 5}
}}

docs = list(fs.collection('foo').where('count', '<', 5).stream())
self.assertEqual({'count': 1}, docs[0].to_dict())

def test_collection_whereLessThanOrEqual(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'count': 1},
'second': {'count': 5}
}}

docs = list(fs.collection('foo').where('count', '<=', 5).stream())
self.assertEqual({'count': 1}, docs[0].to_dict())
self.assertEqual({'count': 5}, docs[1].to_dict())

def test_collection_whereGreaterThan(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'count': 1},
'second': {'count': 5}
}}

docs = list(fs.collection('foo').where('count', '>', 1).stream())
self.assertEqual({'count': 5}, docs[0].to_dict())

def test_collection_whereGreaterThanOrEqual(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'count': 1},
'second': {'count': 5}
}}

docs = list(fs.collection('foo').where('count', '>=', 1).stream())
self.assertEqual({'count': 1}, docs[0].to_dict())
self.assertEqual({'count': 5}, docs[1].to_dict())

def test_collection_whereMissingField(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'count': 1},
'second': {'count': 5}
}}

docs = list(fs.collection('foo').where('no_field', '==', 1).stream())
self.assertEqual(len(docs), 0)

def test_collection_whereNestedField(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'nested': {'a': 1}},
'second': {'nested': {'a': 2}}
}}

docs = list(fs.collection('foo').where('nested.a', '==', 1).stream())
self.assertEqual(len(docs), 1)
self.assertEqual({'nested': {'a': 1}}, docs[0].to_dict())

def test_collection_whereIn(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'field': 'a1'},
'second': {'field': 'a2'},
'third': {'field': 'a3'},
'fourth': {'field': 'a4'},
}}

docs = list(fs.collection('foo').where('field', 'in', ['a1', 'a3']).stream())
self.assertEqual(len(docs), 2)
self.assertEqual({'field': 'a1'}, docs[0].to_dict())
self.assertEqual({'field': 'a3'}, docs[1].to_dict())

def test_collection_whereArrayContains(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'field': ['val4']},
'second': {'field': ['val3', 'val2']},
'third': {'field': ['val3', 'val2', 'val1']}
}}

docs = list(fs.collection('foo').where('field', 'array_contains', 'val1').stream())
self.assertEqual(len(docs), 1)
self.assertEqual(docs[0].to_dict(), {'field': ['val3', 'val2', 'val1']})

def test_collection_whereArrayContainsAny(self):
fs = MockFirestore()
fs._data = {'foo': {
'first': {'field': ['val4']},
'second': {'field': ['val3', 'val2']},
'third': {'field': ['val3', 'val2', 'val1']}
}}

contains_any_docs = list(fs.collection('foo').where('field', 'array_contains_any', ['val1', 'val4']).stream())
self.assertEqual(len(contains_any_docs), 2)
self.assertEqual({'field': ['val4']}, contains_any_docs[0].to_dict())
self.assertEqual({'field': ['val3', 'val2', 'val1']}, contains_any_docs[1].to_dict())
Loading