Skip to content

Commit fba8bda

Browse files
committed
style: apply ruff formatting
Apply ruff formatting to entire codebase. This is a one-time formatting change to adopt modern Python formatting standards. Main changes: - Consistent double quotes for strings - Trailing commas in multi-line function parameters - Standardized spacing and line breaks - Removed trailing whitespace No functional changes - formatting only.
1 parent 26788aa commit fba8bda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+3331
-1281
lines changed

async.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# Async Support for Firebase Functions Python
2+
3+
## Overview
4+
5+
This document outlines the design and implementation plan for adding async function support to firebase-functions-python. The goal is to leverage the new async capabilities in functions-framework while maintaining full backward compatibility with existing sync functions.
6+
7+
## Background
8+
9+
Functions-framework recently added async support via the `--asgi` flag, allowing async functions to be defined like:
10+
11+
```python
12+
import functions_framework.aio
13+
14+
@functions_framework.aio.http
15+
async def hello_async(request): # Starlette.Request
16+
await asyncio.sleep(1)
17+
return "Hello, async world!"
18+
```
19+
20+
## Design Goals
21+
22+
1. **No code duplication** - Reuse existing decorators and logic
23+
2. **Backward compatibility** - All existing sync functions must continue to work
24+
3. **Unified API** - Users shouldn't need different decorators for sync vs async
25+
4. **Type safety** - Proper typing for both sync and async cases
26+
5. **Automatic detection** - The system should automatically detect and handle async functions
27+
6. **Universal support** - Async should work for ALL function types, not just HTTP
28+
29+
## Function Types to Support
30+
31+
Firebase Functions Python supports multiple trigger types that all need async support:
32+
33+
### 1. HTTP Functions
34+
- `@https_fn.on_request()` - Raw HTTP requests
35+
- `@https_fn.on_call()` - Callable functions with auth/validation
36+
37+
### 2. Firestore Functions
38+
- `@firestore_fn.on_document_created()`
39+
- `@firestore_fn.on_document_updated()`
40+
- `@firestore_fn.on_document_deleted()`
41+
- `@firestore_fn.on_document_written()`
42+
43+
### 3. Realtime Database Functions
44+
- `@db_fn.on_value_created()`
45+
- `@db_fn.on_value_updated()`
46+
- `@db_fn.on_value_deleted()`
47+
- `@db_fn.on_value_written()`
48+
49+
### 4. Cloud Storage Functions
50+
- `@storage_fn.on_object_archived()`
51+
- `@storage_fn.on_object_deleted()`
52+
- `@storage_fn.on_object_finalized()`
53+
- `@storage_fn.on_object_metadata_updated()`
54+
55+
### 5. Pub/Sub Functions
56+
- `@pubsub_fn.on_message_published()`
57+
58+
### 6. Scheduler Functions
59+
- `@scheduler_fn.on_schedule()`
60+
61+
### 7. Task Queue Functions
62+
- `@tasks_fn.on_task_dispatched()`
63+
64+
### 8. EventArc Functions
65+
- `@eventarc_fn.on_custom_event_published()`
66+
67+
### 9. Remote Config Functions
68+
- `@remote_config_fn.on_config_updated()`
69+
70+
### 10. Test Lab Functions
71+
- `@test_lab_fn.on_test_matrix_completed()`
72+
73+
### 11. Alerts Functions
74+
- Various alert triggers for billing, crashlytics, performance, etc.
75+
76+
### 12. Identity Functions
77+
- `@identity_fn.before_user_created()`
78+
- `@identity_fn.before_user_signed_in()`
79+
80+
## Implementation Strategy
81+
82+
### Phase 1: Core Infrastructure
83+
84+
#### 1.1 Async Detection Mechanism
85+
- Add utility function to detect if a function is async using `inspect.iscoroutinefunction()`
86+
- This detection should happen at decoration time
87+
88+
#### 1.2 Metadata Storage
89+
- Extend the `__firebase_endpoint__` attribute to include runtime mode information
90+
- Add a field to `ManifestEndpoint` to indicate async functions:
91+
```python
92+
@dataclasses.dataclass(frozen=True)
93+
class ManifestEndpoint:
94+
# ... existing fields ...
95+
runtime_mode: Literal["sync", "async"] | None = "sync"
96+
```
97+
98+
#### 1.3 Type System Updates
99+
- Create type unions to handle both sync and async cases
100+
- For HTTP functions:
101+
- Sync: `flask.Request` and `flask.Response`
102+
- Async: `starlette.requests.Request` and response types
103+
- For event functions:
104+
- Both sync and async will receive the same event objects
105+
- The difference is whether the handler is async
106+
107+
### Phase 2: Decorator Updates
108+
109+
#### 2.1 Universal Decorator Pattern
110+
Each decorator should follow this pattern:
111+
112+
```python
113+
def on_some_event(**kwargs):
114+
def decorator(func):
115+
is_async = inspect.iscoroutinefunction(func)
116+
117+
if is_async:
118+
# Set up async wrapper
119+
@functools.wraps(func)
120+
async def async_wrapper(*args, **kwargs):
121+
# Any necessary async setup
122+
return await func(*args, **kwargs)
123+
124+
wrapped = async_wrapper
125+
runtime_mode = "async"
126+
else:
127+
# Use existing sync wrapper
128+
wrapped = existing_sync_wrapper(func)
129+
runtime_mode = "sync"
130+
131+
# Set metadata
132+
endpoint = create_endpoint(
133+
# ... existing endpoint config ...
134+
runtime_mode=runtime_mode
135+
)
136+
_util.set_func_endpoint_attr(wrapped, endpoint)
137+
138+
return wrapped
139+
140+
return decorator
141+
```
142+
143+
#### 2.2 HTTP Functions Special Handling
144+
HTTP functions need special care because the request type changes:
145+
- Sync: `flask.Request`
146+
- Async: `starlette.requests.Request`
147+
148+
We'll need to handle this in the type system and potentially in request processing.
149+
150+
### Phase 3: Manifest and Deployment
151+
152+
#### 3.1 Manifest Generation
153+
- Update `serving.py` to include runtime mode in the manifest
154+
- The functions.yaml should indicate which functions need async runtime
155+
156+
#### 3.2 Firebase CLI Integration
157+
- The CLI needs to read the runtime mode from the manifest
158+
- When deploying async functions, it should:
159+
- Set appropriate environment variables
160+
- Pass the `--asgi` flag to functions-framework
161+
- Potentially use different container configurations
162+
163+
### Phase 4: Testing and Validation
164+
165+
#### 4.1 Test Coverage
166+
- Add async versions of existing tests
167+
- Test mixed deployments (both sync and async functions)
168+
- Verify proper error handling in async contexts
169+
- Test timeout behavior for async functions
170+
171+
#### 4.2 Example Updates
172+
- Update examples to show async usage
173+
- Create migration guide for converting sync to async
174+
175+
## Example Usage
176+
177+
### HTTP Functions
178+
```python
179+
# Sync (existing)
180+
@https_fn.on_request()
181+
def sync_http(request: Request) -> Response:
182+
return Response("Hello sync")
183+
184+
# Async (new)
185+
@https_fn.on_request()
186+
async def async_http(request) -> Response: # Will be Starlette Request
187+
result = await some_async_api_call()
188+
return Response(f"Hello async: {result}")
189+
```
190+
191+
### Firestore Functions
192+
```python
193+
# Sync (existing)
194+
@firestore_fn.on_document_created(document="users/{userId}")
195+
def sync_user_created(event: Event[DocumentSnapshot]) -> None:
196+
print(f"User created: {event.data.id}")
197+
198+
# Async (new)
199+
@firestore_fn.on_document_created(document="users/{userId}")
200+
async def async_user_created(event: Event[DocumentSnapshot]) -> None:
201+
await send_welcome_email(event.data.get("email"))
202+
await update_analytics(event.data.id)
203+
```
204+
205+
### Pub/Sub Functions
206+
```python
207+
# Async (new)
208+
@pubsub_fn.on_message_published(topic="process-queue")
209+
async def async_process_message(event: CloudEvent[MessagePublishedData]) -> None:
210+
message = event.data.message
211+
await process_job(message.data)
212+
```
213+
214+
## Benefits
215+
216+
1. **Performance**: Async functions can handle I/O-bound operations more efficiently
217+
2. **Scalability**: Better resource utilization for functions that make external API calls
218+
3. **Modern Python**: Aligns with Python's async/await ecosystem
219+
4. **Flexibility**: Users can choose sync or async based on their needs
220+
221+
## Considerations
222+
223+
1. **Cold Start**: Need to verify async functions don't increase cold start times
224+
2. **Memory Usage**: Monitor if async runtime uses more memory
225+
3. **Debugging**: Ensure stack traces and error messages are clear for async functions
226+
4. **Timeouts**: Verify timeout behavior works correctly with async functions
227+
228+
## Migration Path
229+
230+
1. Start with HTTP functions as proof of concept
231+
2. Extend to event-triggered functions
232+
3. Update documentation and examples
233+
4. Release as minor version update (backward compatible)
234+
235+
## Open Questions
236+
237+
1. Should we support both Flask and Starlette response types for async HTTP functions?
238+
2. How should we handle async context managers and cleanup?
239+
3. Should we provide async versions of Firebase Admin SDK operations?
240+
4. What's the best way to handle errors in async functions?
241+
242+
## Next Steps
243+
244+
1. Prototype async support for HTTP functions
245+
2. Test with functions-framework in ASGI mode
246+
3. Design type system for handling both sync and async
247+
4. Update manifest generation
248+
5. Coordinate with Firebase CLI team for deployment support

docs/theme/devsite_translator/html.py

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@
1616
from sphinx.writers import html
1717

1818
_DESCTYPE_NAMES = {
19-
'class': 'Classes',
20-
'data': 'Constants',
21-
'function': 'Functions',
22-
'method': 'Methods',
23-
'attribute': 'Attributes',
24-
'exception': 'Exceptions'
19+
"class": "Classes",
20+
"data": "Constants",
21+
"function": "Functions",
22+
"method": "Methods",
23+
"attribute": "Attributes",
24+
"exception": "Exceptions",
2525
}
2626

2727
# Use the default translator for these node types
28-
_RENDER_WITH_DEFAULT = ['method', 'staticmethod', 'attribute']
28+
_RENDER_WITH_DEFAULT = ["method", "staticmethod", "attribute"]
2929

3030

3131
class FiresiteHTMLTranslator(html.HTMLTranslator):
@@ -39,83 +39,80 @@ class FiresiteHTMLTranslator(html.HTMLTranslator):
3939

4040
def __init__(self, builder, *args, **kwds):
4141
html.HTMLTranslator.__init__(self, builder, *args, **kwds)
42-
self.current_section = 'intro'
42+
self.current_section = "intro"
4343

4444
# This flag gets set to True at the start of a new 'section' tag, and then
4545
# back to False after the first object signature in the section is processed
4646
self.insert_header = False
4747

4848
def visit_desc(self, node):
49-
if node.parent.tagname == 'section':
49+
if node.parent.tagname == "section":
5050
self.insert_header = True
51-
if node['desctype'] != self.current_section:
52-
self.body.append(
53-
f"<h2>{_DESCTYPE_NAMES[node['desctype']]}</h2>")
54-
self.current_section = node['desctype']
55-
if node['desctype'] in _RENDER_WITH_DEFAULT:
51+
if node["desctype"] != self.current_section:
52+
self.body.append(f"<h2>{_DESCTYPE_NAMES[node['desctype']]}</h2>")
53+
self.current_section = node["desctype"]
54+
if node["desctype"] in _RENDER_WITH_DEFAULT:
5655
html.HTMLTranslator.visit_desc(self, node)
5756
else:
58-
self.body.append(self.starttag(node, 'table',
59-
CLASS=node['objtype']))
57+
self.body.append(self.starttag(node, "table", CLASS=node["objtype"]))
6058

6159
def depart_desc(self, node):
62-
if node['desctype'] in _RENDER_WITH_DEFAULT:
60+
if node["desctype"] in _RENDER_WITH_DEFAULT:
6361
html.HTMLTranslator.depart_desc(self, node)
6462
else:
65-
self.body.append('</table>\n\n')
63+
self.body.append("</table>\n\n")
6664

6765
def visit_desc_signature(self, node):
68-
if node.parent['desctype'] in _RENDER_WITH_DEFAULT:
66+
if node.parent["desctype"] in _RENDER_WITH_DEFAULT:
6967
html.HTMLTranslator.visit_desc_signature(self, node)
7068
else:
71-
self.body.append('<tr>')
72-
self.body.append(self.starttag(node, 'th'))
69+
self.body.append("<tr>")
70+
self.body.append(self.starttag(node, "th"))
7371
if self.insert_header:
74-
self.body.append(
75-
f"<h3 class=\"sphinx-hidden\">{node['fullname']}</h3>")
72+
self.body.append(f'<h3 class="sphinx-hidden">{node["fullname"]}</h3>')
7673
self.insert_header = False
7774

7875
def depart_desc_signature(self, node):
79-
if node.parent['desctype'] in _RENDER_WITH_DEFAULT:
76+
if node.parent["desctype"] in _RENDER_WITH_DEFAULT:
8077
html.HTMLTranslator.depart_desc_signature(self, node)
8178
else:
82-
self.body.append('</th></tr>')
79+
self.body.append("</th></tr>")
8380

8481
def visit_desc_content(self, node):
85-
if node.parent['desctype'] in _RENDER_WITH_DEFAULT:
82+
if node.parent["desctype"] in _RENDER_WITH_DEFAULT:
8683
html.HTMLTranslator.visit_desc_content(self, node)
8784
else:
88-
self.body.append('<tr>')
89-
self.body.append(self.starttag(node, 'td'))
85+
self.body.append("<tr>")
86+
self.body.append(self.starttag(node, "td"))
9087

9188
def depart_desc_content(self, node):
92-
if node.parent['desctype'] in _RENDER_WITH_DEFAULT:
89+
if node.parent["desctype"] in _RENDER_WITH_DEFAULT:
9390
html.HTMLTranslator.depart_desc_content(self, node)
9491
else:
95-
self.body.append('</td></tr>')
92+
self.body.append("</td></tr>")
9693

9794
def visit_title(self, node):
98-
if node.parent.tagname == 'section':
95+
if node.parent.tagname == "section":
9996
self.body.append('<h1 class="page-title">')
10097
else:
10198
html.HTMLTranslator.visit_title(self, node)
10299

103100
def depart_title(self, node):
104-
if node.parent.tagname == 'section':
105-
self.body.append('</h1>')
101+
if node.parent.tagname == "section":
102+
self.body.append("</h1>")
106103
else:
107104
html.HTMLTranslator.depart_title(self, node)
108105

109106
def visit_note(self, node):
110-
self.body.append(self.starttag(node, 'aside', CLASS='note'))
107+
self.body.append(self.starttag(node, "aside", CLASS="note"))
111108

112109
def depart_note(self, node):
113110
# pylint: disable=unused-argument
114-
self.body.append('</aside>\n\n')
111+
self.body.append("</aside>\n\n")
115112

116113
def visit_warning(self, node):
117-
self.body.append(self.starttag(node, 'aside', CLASS='caution'))
114+
self.body.append(self.starttag(node, "aside", CLASS="caution"))
118115

119116
def depart_warning(self, node):
120117
# pylint: disable=unused-argument
121-
self.body.append('</aside>\n\n')
118+
self.body.append("</aside>\n\n")

0 commit comments

Comments
 (0)