-
Notifications
You must be signed in to change notification settings - Fork 85
feat: add pipelines structure #1046
Changes from 128 commits
1087d64
053f55b
e56459c
babafb2
149c61f
320cea1
cd2963d
3cbc2d5
0c2241d
9c8982c
863bd1d
7cd2c11
c328934
de0a862
45c83aa
fc57f04
0224f94
cdee937
5d35c54
11373e3
d2b4153
bfdfba3
afa823a
3348127
e3c995d
2d604e1
3bed807
9b9eaa8
e35accf
72301a1
07d25dc
38fb7fa
0c3852f
1d95e54
a3daa07
8e45b68
6b2a605
6d2aa90
e51d9dc
8f6dea5
654e5f7
69d4b76
6d966a8
33fee52
cb8539d
380dce9
831886f
79df205
e760c44
ea1e2ba
b6b082d
658964c
7e12b74
e463418
4f527c1
a323e5b
7e70022
98887a0
549c590
376015c
e3de1b8
f725bc7
128ab1c
64a8bda
ec06080
0703532
246d1c8
b72d44a
810ccd2
78b5833
36e0228
43f4cf4
51e9b25
35cb0bb
6283d1a
080bf42
6cd5c63
7143247
05c4f23
7512a1a
901c0e1
6a5f72e
072669f
05a7498
9f8d4c8
863a09d
e9f11c6
d2c33f2
db1ecaa
b6d74da
70efea7
b36afa8
7a26f7e
fee6c90
da18289
fc80062
c049e21
41b91d4
7f69229
0b4c294
255b698
bd9c2c4
d8dc10f
1f2390a
39f261a
48bf9f7
b57424d
8fde414
9cac154
93003c0
79d016a
907a551
8e20c11
98c7ea5
af4fb20
cd38fc2
0ac319e
7981a34
62b6510
035c6e6
5dd1246
6434023
a8beea4
2d286bb
d2babd2
b46bdc1
22b558c
3432322
64cd4fb
e74e04d
a818f52
8a9c3ec
06a2084
13389b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # Copyright 2025 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
| from typing import AsyncIterable, TYPE_CHECKING | ||
| from google.cloud.firestore_v1 import pipeline_stages as stages | ||
| from google.cloud.firestore_v1.base_pipeline import _BasePipeline | ||
|
|
||
| if TYPE_CHECKING: # pragma: NO COVER | ||
| from google.cloud.firestore_v1.async_client import AsyncClient | ||
| from google.cloud.firestore_v1.pipeline_result import PipelineResult | ||
| from google.cloud.firestore_v1.async_transaction import AsyncTransaction | ||
|
|
||
|
|
||
| class AsyncPipeline(_BasePipeline): | ||
| """ | ||
| Pipelines allow for complex data transformations and queries involving | ||
| multiple stages like filtering, projection, aggregation, and vector search. | ||
|
|
||
| This class extends `_BasePipeline` and provides methods to execute the | ||
| defined pipeline stages using an asynchronous `AsyncClient`. | ||
|
|
||
| Usage Example: | ||
| >>> from google.cloud.firestore_v1.pipeline_expressions import Field | ||
| >>> | ||
| >>> async def run_pipeline(): | ||
| ... client = AsyncClient(...) | ||
| ... pipeline = client.pipeline() | ||
| ... .collection("books") | ||
| ... .where(Field.of("published").gt(1980)) | ||
| ... .select("title", "author") | ||
| ... async for result in pipeline.execute_async(): | ||
| ... print(result) | ||
|
|
||
| Use `client.pipeline()` to create instances of this class. | ||
| """ | ||
|
|
||
| def __init__(self, client: AsyncClient, *stages: stages.Stage): | ||
| """ | ||
| Initializes an asynchronous Pipeline. | ||
|
|
||
| Args: | ||
| client: The asynchronous `AsyncClient` instance to use for execution. | ||
| *stages: Initial stages for the pipeline. | ||
| """ | ||
| super().__init__(client, *stages) | ||
|
|
||
| async def execute( | ||
| self, | ||
| transaction: "AsyncTransaction" | None = None, | ||
| ) -> AsyncIterable[PipelineResult]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with how firestore for Python works, but within the SDKs my team works on, execute would buffer all of the PipelineResults into memory and make these available as an array. The execute method itself would be asyncronous. However, our server sdks, java-firestore and nodejs-firstore, will offer a streaming API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, FWIW, we will be adding a wrapper object PipelineSnapshot which contains all results and other metadata (like explain info and timestamps). I will be sending your team an updated nodejs-firestore PR with our latest changes, as soon as i can.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, that makes sense, good feedback. I will create separate methods for |
||
| """ | ||
| Executes this pipeline, providing results through an Iterable | ||
|
|
||
| Args: | ||
| transaction | ||
| (Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]): | ||
| An existing transaction that this query will run in. | ||
| If a ``transaction`` is used and it already has write operations | ||
| added, this method cannot be used (i.e. read-after-write is not | ||
| allowed). | ||
| """ | ||
| request = self._prep_execute_request(transaction) | ||
| async for response in await self._client._firestore_api.execute_pipeline( | ||
| request | ||
| ): | ||
| for result in self._execute_response_helper(response): | ||
| yield result | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Copyright 2025 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
| from typing import Iterable, TYPE_CHECKING | ||
| from google.cloud.firestore_v1 import pipeline_stages as stages | ||
| from google.cloud.firestore_v1.types.pipeline import ( | ||
| StructuredPipeline as StructuredPipeline_pb, | ||
| ) | ||
| from google.cloud.firestore_v1.types.firestore import ExecutePipelineRequest | ||
| from google.cloud.firestore_v1.pipeline_result import PipelineResult | ||
| from google.cloud.firestore_v1 import _helpers | ||
|
|
||
| if TYPE_CHECKING: # pragma: NO COVER | ||
| from google.cloud.firestore_v1.client import Client | ||
| from google.cloud.firestore_v1.async_client import AsyncClient | ||
| from google.cloud.firestore_v1.types.firestore import ExecutePipelineResponse | ||
| from google.cloud.firestore_v1.transaction import BaseTransaction | ||
|
|
||
|
|
||
| class _BasePipeline: | ||
| """ | ||
| Base class for building Firestore data transformation and query pipelines. | ||
|
|
||
| This class is not intended to be instantiated directly. | ||
| Use `client.collection.("...").pipeline()` to create pipeline instances. | ||
| """ | ||
|
|
||
| def __init__(self, client: Client | AsyncClient, *stages: stages.Stage): | ||
| """ | ||
| Initializes a new pipeline with the given stages. | ||
|
|
||
| Pipeline classes should not be instantiated directly. | ||
|
|
||
| Args: | ||
| client: The client associated with the pipeline | ||
| *stages: Initial stages for the pipeline. | ||
| """ | ||
| self._client = client | ||
| self.stages = tuple(stages) | ||
|
|
||
| def __repr__(self): | ||
| cls_str = type(self).__name__ | ||
| if not self.stages: | ||
| return f"{cls_str}()" | ||
| elif len(self.stages) == 1: | ||
| return f"{cls_str}({self.stages[0]!r})" | ||
| else: | ||
| stages_str = ",\n ".join([repr(s) for s in self.stages]) | ||
| return f"{cls_str}(\n {stages_str}\n)" | ||
|
|
||
| def _to_pb(self) -> StructuredPipeline_pb: | ||
| return StructuredPipeline_pb( | ||
| pipeline={"stages": [s._to_pb() for s in self.stages]} | ||
| ) | ||
|
|
||
| def _append(self, new_stage): | ||
| """ | ||
| Create a new Pipeline object with a new stage appended | ||
| """ | ||
| return self.__class__(self._client, *self.stages, new_stage) | ||
|
|
||
| def _prep_execute_request( | ||
| self, transaction: BaseTransaction | None | ||
| ) -> ExecutePipelineRequest: | ||
| """ | ||
| shared logic for creating an ExecutePipelineRequest | ||
| """ | ||
| database_name = ( | ||
| f"projects/{self._client.project}/databases/{self._client._database}" | ||
| ) | ||
| transaction_id = ( | ||
| _helpers.get_transaction_id(transaction) | ||
| if transaction is not None | ||
| else None | ||
| ) | ||
| request = ExecutePipelineRequest( | ||
| database=database_name, | ||
| transaction=transaction_id, | ||
| structured_pipeline=self._to_pb(), | ||
| ) | ||
| return request | ||
|
|
||
| def _execute_response_helper( | ||
| self, response: ExecutePipelineResponse | ||
| ) -> Iterable[PipelineResult]: | ||
| """ | ||
| shared logic for unpacking an ExecutePipelineReponse into PipelineResults | ||
| """ | ||
| for doc in response.results: | ||
| ref = self._client.document(doc.name) if doc.name else None | ||
| yield PipelineResult( | ||
| self._client, | ||
| doc.fields, | ||
| ref, | ||
| response._pb.execution_time, | ||
| doc._pb.create_time if doc.create_time else None, | ||
| doc._pb.update_time if doc.update_time else None, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # Copyright 2025 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
| from typing import Iterable, TYPE_CHECKING | ||
| from google.cloud.firestore_v1 import pipeline_stages as stages | ||
| from google.cloud.firestore_v1.base_pipeline import _BasePipeline | ||
|
|
||
| if TYPE_CHECKING: # pragma: NO COVER | ||
| from google.cloud.firestore_v1.client import Client | ||
| from google.cloud.firestore_v1.pipeline_result import PipelineResult | ||
| from google.cloud.firestore_v1.transaction import Transaction | ||
|
|
||
|
|
||
| class Pipeline(_BasePipeline): | ||
| """ | ||
| Pipelines allow for complex data transformations and queries involving | ||
| multiple stages like filtering, projection, aggregation, and vector search. | ||
|
|
||
| Usage Example: | ||
| >>> from google.cloud.firestore_v1.pipeline_expressions import Field | ||
| >>> | ||
| >>> def run_pipeline(): | ||
| ... client = Client(...) | ||
| ... pipeline = client.pipeline() | ||
| ... .collection("books") | ||
| ... .where(Field.of("published").gt(1980)) | ||
| ... .select("title", "author") | ||
| ... for result in pipeline.execute(): | ||
| ... print(result) | ||
|
|
||
| Use `client.pipeline()` to create instances of this class. | ||
| """ | ||
|
|
||
| def __init__(self, client: Client, *stages: stages.Stage): | ||
| """ | ||
| Initializes a Pipeline. | ||
|
|
||
| Args: | ||
| client: The `Client` instance to use for execution. | ||
| *stages: Initial stages for the pipeline. | ||
| """ | ||
| super().__init__(client, *stages) | ||
|
|
||
| def execute( | ||
| self, | ||
| transaction: "Transaction" | None = None, | ||
| ) -> Iterable[PipelineResult]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the synchronous iterable here. Makes sense. But I'm still wondering if there is an in-between impementation, where the execute is asynchronous, but the results are available in a synchronous iterable. Also, same comment about returning a PipelineSnapshot
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think Python really provides a way to do this, because synchronous iterables are inherently blocking. But I'm interested if you have an idea in mind |
||
| """ | ||
| Executes this pipeline, providing results through an Iterable | ||
|
|
||
| Args: | ||
| transaction | ||
| (Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]): | ||
| An existing transaction that this query will run in. | ||
| If a ``transaction`` is used and it already has write operations | ||
| added, this method cannot be used (i.e. read-after-write is not | ||
| allowed). | ||
| """ | ||
| request = self._prep_execute_request(transaction) | ||
| for response in self._client._firestore_api.execute_pipeline(request): | ||
| yield from self._execute_response_helper(response) | ||
Uh oh!
There was an error while loading. Please reload this page.