Skip to content

Commit 5d7b19f

Browse files
committed
Refactor Firebase function implementation and update dependencies
1 parent 7ce83b1 commit 5d7b19f

5 files changed

Lines changed: 215 additions & 6 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
*.local
1+
*.local
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from enum import Enum
2+
from typing import Any, Dict, Callable, TypeVar, Union, Optional
3+
from google.cloud.firestore import DocumentSnapshot, GeoPoint, SERVER_TIMESTAMP
4+
from firebase_functions.firestore_fn import Event, Change
5+
6+
# Type definitions
7+
FirestoreField = Union[str, int, float, bool, dict, list, GeoPoint, None]
8+
9+
class ChangeType(str, Enum):
10+
CREATE = "CREATE"
11+
UPDATE = "UPDATE"
12+
DELETE = "DELETE"
13+
14+
class State(str, Enum):
15+
PROCESSING = "PROCESSING"
16+
COMPLETED = "COMPLETED"
17+
ERROR = "ERROR"
18+
19+
def now():
20+
return SERVER_TIMESTAMP
21+
22+
def get_change_type(change: Change) -> ChangeType:
23+
if not change.before or not change.before.exists:
24+
return ChangeType.CREATE
25+
if not change.after or not change.after.exists:
26+
return ChangeType.DELETE
27+
return ChangeType.UPDATE
28+
29+
def is_delete(change: Change) -> bool:
30+
return get_change_type(change) == ChangeType.DELETE
31+
32+
def is_update(change: Change) -> bool:
33+
return get_change_type(change) == ChangeType.UPDATE
34+
35+
def is_create(change: Change) -> bool:
36+
return get_change_type(change) == ChangeType.CREATE
37+
38+
def safe_get(document, field_path, default=None):
39+
"""Safely get a field from a document without KeyError."""
40+
if document is None:
41+
return default
42+
if not hasattr(document, 'get'):
43+
return default
44+
45+
try:
46+
return document.get(field_path)
47+
except KeyError:
48+
return default
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from typing import Any, Dict, Callable, TypeVar, Generic, Optional, Awaitable
2+
from firebase_functions.firestore_fn import Event, Change, DocumentSnapshot
3+
from .common import ChangeType, State, now, get_change_type, FirestoreField, safe_get
4+
5+
T = TypeVar('T')
6+
TOutput = TypeVar('TOutput', bound=Dict[str, Any])
7+
8+
class ProcessConfig:
9+
def __init__(
10+
self,
11+
input_field: str,
12+
process_fn: Callable[[Any, Event], Awaitable[Dict[str, FirestoreField]]],
13+
error_fn: Callable[[Any], str],
14+
status_field: Optional[str] = None,
15+
order_field: Optional[str] = None
16+
):
17+
self.input_field = input_field
18+
self.process_fn = process_fn
19+
self.error_fn = error_fn
20+
self.status_field = status_field
21+
self.order_field = order_field
22+
23+
class FirestoreOnWriteProcessor(Generic[T, TOutput]):
24+
def __init__(self, options: ProcessConfig):
25+
self.input_field = options.input_field or 'prompt'
26+
self.order_field = options.order_field or 'createTime'
27+
self.status_field = options.status_field or 'status'
28+
self.process_fn = options.process_fn
29+
self.process_updates = True
30+
self.error_fn = options.error_fn
31+
32+
def should_process(self, change: Change) -> bool:
33+
change_type = get_change_type(change)
34+
if change_type == ChangeType.DELETE:
35+
return False
36+
37+
# Extract status if it exists
38+
status = safe_get(change.after, self.status_field)
39+
state: State = status.get("state") if isinstance(status, dict) else None
40+
new_value = safe_get(change.after, self.input_field)
41+
old_value = safe_get(change.before, self.input_field)
42+
43+
has_changed = (
44+
change_type == ChangeType.CREATE or
45+
(self.process_updates and
46+
change_type == ChangeType.UPDATE and
47+
old_value != new_value)
48+
)
49+
50+
if (
51+
not new_value or
52+
state in [State.PROCESSING.value, State.COMPLETED.value, State.ERROR.value] or
53+
not has_changed or
54+
not isinstance(new_value, str)
55+
):
56+
return False
57+
58+
return True
59+
60+
async def write_start_event(self, event: Event[Change[DocumentSnapshot]]) -> None:
61+
create_time = event.data.after.create_time
62+
update_time = now()
63+
64+
status = {
65+
"state": State.PROCESSING.value,
66+
"startTime": update_time,
67+
"updateTime": update_time
68+
}
69+
70+
start_data = safe_get(event.data.after, self.order_field)
71+
# Prepare update data
72+
if start_data:
73+
update_data = {self.status_field: status}
74+
else:
75+
update_data = {
76+
self.order_field: create_time,
77+
self.status_field: status
78+
}
79+
80+
event.data.after.reference.update(update_data)
81+
82+
async def write_completion_event(self, change: Change, output: Dict[str, Any]) -> None:
83+
update_time = now()
84+
85+
# In Firebase Python, we need to use dot notation as strings
86+
update_data = dict(output) # Create a copy to avoid modifying the original
87+
update_data[f"{self.status_field}.state"] = State.COMPLETED.value
88+
update_data[f"{self.status_field}.updateTime"] = update_time
89+
update_data[f"{self.status_field}.completeTime"] = update_time
90+
91+
change.after.reference.update(update_data)
92+
93+
async def write_error_event(self, change: Change, e: Any) -> None:
94+
event_timestamp = now()
95+
error_message = self.error_fn(e)
96+
97+
change.after.reference.update({
98+
self.status_field: {
99+
"state": State.ERROR.value,
100+
"updateTime": event_timestamp,
101+
"error": error_message
102+
}
103+
})
104+
105+
async def run(self, event: Event) -> None:
106+
if not event:
107+
print("No event data")
108+
return
109+
110+
if not event.data:
111+
print("No document event.data")
112+
return
113+
114+
if not self.should_process(event.data):
115+
return
116+
117+
try:
118+
await self.write_start_event(event)
119+
120+
input_data = safe_get(event.data.after, self.input_field)
121+
output = await self.process_fn(input_data, event)
122+
123+
await self.write_completion_event(event.data, output)
124+
except Exception as e:
125+
print(f"Message processing error: {e}")
126+
await self.write_error_event(event.data, e)
Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,46 @@
1-
# Welcome to Cloud Functions for Firebase for Python!
2-
# To get started, simply uncomment the below code or create your own.
3-
# Deploy with `firebase deploy`
1+
import asyncio
42

5-
from firebase_functions import https_fn
3+
from firebase_functions import https_fn, firestore_fn
64
from firebase_admin import initialize_app
5+
from typing import Dict, Any
6+
from firestore_onwrite_processor.processor import (
7+
FirestoreOnWriteProcessor,
8+
ProcessConfig,
9+
)
10+
from firestore_onwrite_processor.common import FirestoreField
711

812
initialize_app()
913

1014

15+
# Example implementation of a process function
16+
async def process_document(
17+
input_value: str, event: firestore_fn.Event
18+
) -> Dict[str, FirestoreField]:
19+
# Your processing logic here
20+
print(f"Processing document with input: {input_value}")
21+
return {"result": f"Processed: {input_value}"}
22+
23+
24+
# Example error handler
25+
def handle_error(error: Any) -> str:
26+
return f"Processing error: {str(error)}"
27+
28+
29+
# Create the processor
30+
processor_config = ProcessConfig(
31+
input_field="prompt", process_fn=process_document, error_fn=handle_error
32+
)
33+
34+
document_processor = FirestoreOnWriteProcessor(processor_config)
35+
36+
37+
@firestore_fn.on_document_written(document="collection/{docId}")
38+
def process_firestore_document(event: firestore_fn.Event) -> None:
39+
"""Process a document when it's written to Firestore."""
40+
asyncio.run(document_processor.run(event))
41+
42+
43+
# example function
1144
@https_fn.on_request()
1245
def on_request_example(req: https_fn.Request) -> https_fn.Response:
1346
return https_fn.Response("Hello world!")
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
firebase_functions~=0.1.0
1+
firebase_functions>=0.4.2
2+
firebase_admin>=6.7.0
3+
# google-cloud-firestore>=2.11.0

0 commit comments

Comments
 (0)