diff --git a/.github/workflows/pr_agent.yml b/.github/workflows/pr_agent.yml new file mode 100644 index 0000000..ed484cd --- /dev/null +++ b/.github/workflows/pr_agent.yml @@ -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/pr-agent@v0.26 + with: + args: '/improve --pr_code_suggestions.commitable_code_suggestions=true' + env: + OPENAI_KEY: ${{ secrets.OPENAI_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41a7cd6..dcf8686 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore index 2f5187d..e293bc6 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ venv.bak/ .mypy_cache/ .idea/ +.qodo diff --git a/agent-coding-standards.md b/agent-coding-standards.md new file mode 100644 index 0000000..a30b148 --- /dev/null +++ b/agent-coding-standards.md @@ -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 diff --git a/mockfirestore/collection.py b/mockfirestore/collection.py index 431c074..1361cc5 100644 --- a/mockfirestore/collection.py +++ b/mockfirestore/collection.py @@ -3,7 +3,7 @@ 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.query import Query, FieldFilter from mockfirestore.document import DocumentReference, DocumentSnapshot @@ -41,9 +41,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, filter.op, 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)]) @@ -82,4 +83,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 \ No newline at end of file diff --git a/mockfirestore/query.py b/mockfirestore/query.py index 7a4618d..dae89e1 100644 --- a/mockfirestore/query.py +++ b/mockfirestore/query.py @@ -1,10 +1,22 @@ import warnings from itertools import islice, tee from typing import Iterator, Any, Optional, List, Callable, Union +from dataclasses import dataclass from mockfirestore.document import DocumentSnapshot from mockfirestore._helpers import T +@dataclass +class FieldFilter: + field: str + op: str + value: Any + + def __init__(self, field: str, op: str, value: Any): + self.field = field + self.op = op + self.value = value + class Query: def __init__(self, parent: 'CollectionReference', projection=None, @@ -61,8 +73,11 @@ 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, filter.op, filter.value) + else: + self._add_field_filter(field, op, value) return self def order_by(self, key: str, direction: Optional[str] = 'ASCENDING') -> 'Query': @@ -136,4 +151,4 @@ def _compare_func(self, op: str) -> Callable[[T, T], bool]: elif op == 'array_contains': return lambda x, y: y in x elif op == 'array_contains_any': - return lambda x, y: any([val in y for val in x]) + return lambda x, y: any([val in y for val in x]) \ No newline at end of file diff --git a/requirements-dev-minimal.txt b/requirements-dev-minimal.txt index 38604d8..aacb83a 100644 --- a/requirements-dev-minimal.txt +++ b/requirements-dev-minimal.txt @@ -1 +1 @@ -google-cloud-firestore \ No newline at end of file +google-cloud-firestore diff --git a/setup.py b/setup.py index f55cb88..326d1cc 100644 --- a/setup.py +++ b/setup.py @@ -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, @@ -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", ], ) \ No newline at end of file diff --git a/tests/test_where_field.py b/tests/test_where_field.py new file mode 100644 index 0000000..8b8bd51 --- /dev/null +++ b/tests/test_where_field.py @@ -0,0 +1,138 @@ +from unittest import TestCase + +from mockfirestore import MockFirestore +from mockfirestore.query import FieldFilter + + +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()) \ No newline at end of file