atlan-application-sdk 0.1.1rc39__py3-none-any.whl → 0.1.1rc41__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- application_sdk/activities/.cursor/BUGBOT.md +424 -0
- application_sdk/activities/metadata_extraction/sql.py +400 -25
- application_sdk/application/__init__.py +2 -0
- application_sdk/application/metadata_extraction/sql.py +3 -0
- application_sdk/clients/.cursor/BUGBOT.md +280 -0
- application_sdk/clients/models.py +42 -0
- application_sdk/clients/sql.py +127 -87
- application_sdk/clients/temporal.py +3 -1
- application_sdk/common/.cursor/BUGBOT.md +316 -0
- application_sdk/common/aws_utils.py +259 -11
- application_sdk/common/utils.py +145 -9
- application_sdk/constants.py +8 -0
- application_sdk/decorators/.cursor/BUGBOT.md +279 -0
- application_sdk/handlers/__init__.py +8 -1
- application_sdk/handlers/sql.py +63 -22
- application_sdk/inputs/.cursor/BUGBOT.md +250 -0
- application_sdk/interceptors/.cursor/BUGBOT.md +320 -0
- application_sdk/interceptors/cleanup.py +171 -0
- application_sdk/interceptors/events.py +6 -6
- application_sdk/observability/decorators/observability_decorator.py +36 -22
- application_sdk/outputs/.cursor/BUGBOT.md +295 -0
- application_sdk/outputs/iceberg.py +4 -0
- application_sdk/outputs/json.py +6 -0
- application_sdk/outputs/parquet.py +13 -3
- application_sdk/server/.cursor/BUGBOT.md +442 -0
- application_sdk/server/fastapi/__init__.py +59 -3
- application_sdk/server/fastapi/models.py +27 -0
- application_sdk/services/objectstore.py +16 -3
- application_sdk/version.py +1 -1
- application_sdk/workflows/.cursor/BUGBOT.md +218 -0
- {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/METADATA +1 -1
- {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/RECORD +35 -24
- {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/licenses/NOTICE +0 -0
application_sdk/outputs/json.py
CHANGED
|
@@ -93,6 +93,7 @@ class JsonOutput(Output):
|
|
|
93
93
|
path_gen: Callable[[int | None, int], str] = path_gen,
|
|
94
94
|
start_marker: Optional[str] = None,
|
|
95
95
|
end_marker: Optional[str] = None,
|
|
96
|
+
retain_local_copy: bool = False,
|
|
96
97
|
**kwargs: Dict[str, Any],
|
|
97
98
|
):
|
|
98
99
|
"""Initialize the JSON output handler.
|
|
@@ -113,6 +114,8 @@ class JsonOutput(Output):
|
|
|
113
114
|
Defaults to 0.
|
|
114
115
|
path_gen (Callable, optional): Function to generate file paths.
|
|
115
116
|
Defaults to path_gen function.
|
|
117
|
+
retain_local_copy (bool, optional): Whether to retain the local copy of the files.
|
|
118
|
+
Defaults to False.
|
|
116
119
|
"""
|
|
117
120
|
self.output_path = output_path
|
|
118
121
|
self.output_suffix = output_suffix
|
|
@@ -133,6 +136,7 @@ class JsonOutput(Output):
|
|
|
133
136
|
self.start_marker = start_marker
|
|
134
137
|
self.end_marker = end_marker
|
|
135
138
|
self.metrics = get_metrics()
|
|
139
|
+
self.retain_local_copy = retain_local_copy
|
|
136
140
|
|
|
137
141
|
if not self.output_path:
|
|
138
142
|
raise ValueError("output_path is required")
|
|
@@ -282,6 +286,7 @@ class JsonOutput(Output):
|
|
|
282
286
|
await ObjectStore.upload_prefix(
|
|
283
287
|
source=self.output_path,
|
|
284
288
|
destination=get_object_store_prefix(self.output_path),
|
|
289
|
+
retain_local_copy=self.retain_local_copy,
|
|
285
290
|
)
|
|
286
291
|
|
|
287
292
|
except Exception as e:
|
|
@@ -367,6 +372,7 @@ class JsonOutput(Output):
|
|
|
367
372
|
await ObjectStore.upload_file(
|
|
368
373
|
source=output_file_name,
|
|
369
374
|
destination=get_object_store_prefix(output_file_name),
|
|
375
|
+
retain_local_copy=self.retain_local_copy,
|
|
370
376
|
)
|
|
371
377
|
|
|
372
378
|
self.buffer.clear()
|
|
@@ -60,6 +60,7 @@ class ParquetOutput(Output):
|
|
|
60
60
|
chunk_start: Optional[int] = None,
|
|
61
61
|
start_marker: Optional[str] = None,
|
|
62
62
|
end_marker: Optional[str] = None,
|
|
63
|
+
retain_local_copy: bool = False,
|
|
63
64
|
):
|
|
64
65
|
"""Initialize the Parquet output handler.
|
|
65
66
|
|
|
@@ -79,6 +80,8 @@ class ParquetOutput(Output):
|
|
|
79
80
|
Defaults to None.
|
|
80
81
|
end_marker (Optional[str], optional): End marker for query extraction.
|
|
81
82
|
Defaults to None.
|
|
83
|
+
retain_local_copy (bool, optional): Whether to retain the local copy of the files.
|
|
84
|
+
Defaults to False.
|
|
82
85
|
"""
|
|
83
86
|
self.output_path = output_path
|
|
84
87
|
self.output_suffix = output_suffix
|
|
@@ -99,6 +102,7 @@ class ParquetOutput(Output):
|
|
|
99
102
|
self.end_marker = end_marker
|
|
100
103
|
self.statistics = []
|
|
101
104
|
self.metrics = get_metrics()
|
|
105
|
+
self.retain_local_copy = retain_local_copy
|
|
102
106
|
|
|
103
107
|
# Create output directory
|
|
104
108
|
self.output_path = os.path.join(self.output_path, self.output_suffix)
|
|
@@ -295,13 +299,19 @@ class ParquetOutput(Output):
|
|
|
295
299
|
# Upload the entire directory (contains multiple parquet files created by Daft)
|
|
296
300
|
if write_mode == WriteMode.OVERWRITE:
|
|
297
301
|
# Delete the directory from object store
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
try:
|
|
303
|
+
await ObjectStore.delete_prefix(
|
|
304
|
+
prefix=get_object_store_prefix(self.output_path)
|
|
305
|
+
)
|
|
306
|
+
except FileNotFoundError as e:
|
|
307
|
+
logger.info(
|
|
308
|
+
f"No files found under prefix {get_object_store_prefix(self.output_path)}: {str(e)}"
|
|
309
|
+
)
|
|
301
310
|
|
|
302
311
|
await ObjectStore.upload_prefix(
|
|
303
312
|
source=self.output_path,
|
|
304
313
|
destination=get_object_store_prefix(self.output_path),
|
|
314
|
+
retain_local_copy=self.retain_local_copy,
|
|
305
315
|
)
|
|
306
316
|
|
|
307
317
|
except Exception as e:
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# Server Code Review Guidelines - FastAPI Applications
|
|
2
|
+
|
|
3
|
+
## Context-Specific Patterns
|
|
4
|
+
|
|
5
|
+
This directory contains FastAPI server implementations, middleware, routers, and API endpoint definitions. These components handle HTTP requests, authentication, and API responses.
|
|
6
|
+
|
|
7
|
+
### Phase 1: Critical Server Safety Issues
|
|
8
|
+
|
|
9
|
+
**API Security Requirements:**
|
|
10
|
+
|
|
11
|
+
- All endpoints must have proper input validation using Pydantic models
|
|
12
|
+
- Authentication and authorization must be enforced on protected endpoints
|
|
13
|
+
- No sensitive data in API responses (passwords, tokens, internal IDs)
|
|
14
|
+
- Request rate limiting must be implemented for public endpoints
|
|
15
|
+
- CORS configuration must be explicit and restrictive
|
|
16
|
+
|
|
17
|
+
**Input Validation and Sanitization:**
|
|
18
|
+
|
|
19
|
+
- All request bodies must use Pydantic models for validation
|
|
20
|
+
- Query parameters must be validated with proper types
|
|
21
|
+
- File uploads must have size and type restrictions
|
|
22
|
+
- SQL injection prevention in any database queries
|
|
23
|
+
- No raw user input in log messages
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# ✅ DO: Proper input validation
|
|
27
|
+
from pydantic import BaseModel, Field, validator
|
|
28
|
+
|
|
29
|
+
class CreateUserRequest(BaseModel):
|
|
30
|
+
username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_]+$")
|
|
31
|
+
email: str = Field(..., regex=r'^[\w\.-]+@[\w\.-]+\.\w+$')
|
|
32
|
+
age: int = Field(..., ge=18, le=120)
|
|
33
|
+
|
|
34
|
+
@validator('username')
|
|
35
|
+
def username_must_not_contain_prohibited_words(cls, v):
|
|
36
|
+
prohibited = ['admin', 'root', 'system']
|
|
37
|
+
if any(word in v.lower() for word in prohibited):
|
|
38
|
+
raise ValueError('Username contains prohibited words')
|
|
39
|
+
return v
|
|
40
|
+
|
|
41
|
+
@app.post("/users/", response_model=UserResponse)
|
|
42
|
+
async def create_user(user_data: CreateUserRequest):
|
|
43
|
+
# Input is already validated by Pydantic
|
|
44
|
+
return await user_service.create_user(user_data)
|
|
45
|
+
|
|
46
|
+
# ❌ NEVER: Raw input without validation
|
|
47
|
+
@app.post("/users/")
|
|
48
|
+
async def bad_create_user(request: dict): # No validation!
|
|
49
|
+
username = request.get("username") # Could be anything
|
|
50
|
+
return await user_service.create_user(username)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Phase 2: FastAPI Architecture Patterns
|
|
54
|
+
|
|
55
|
+
**Router Organization:**
|
|
56
|
+
|
|
57
|
+
- Group related endpoints in separate router modules
|
|
58
|
+
- Use consistent URL patterns and naming conventions
|
|
59
|
+
- Implement proper HTTP status codes for all responses
|
|
60
|
+
- Use response models for all endpoint returns
|
|
61
|
+
- Implement proper error handling with HTTP exceptions
|
|
62
|
+
|
|
63
|
+
**Dependency Injection:**
|
|
64
|
+
|
|
65
|
+
- Use FastAPI's dependency injection for database connections
|
|
66
|
+
- Implement proper dependency scoping (request, application)
|
|
67
|
+
- Create reusable dependencies for authentication, logging, etc.
|
|
68
|
+
- Use dependency override for testing
|
|
69
|
+
- Implement proper cleanup for dependencies
|
|
70
|
+
|
|
71
|
+
**Async Pattern Enforcement:**
|
|
72
|
+
|
|
73
|
+
- **Always use async/await for I/O operations**: Database queries, external API calls, file operations
|
|
74
|
+
- **Non-blocking operations**: Ensure that async endpoints don't accidentally use blocking operations
|
|
75
|
+
- **Proper context switching**: Use async context managers for resource management
|
|
76
|
+
- **Background task usage**: Use FastAPI BackgroundTasks for non-critical operations that shouldn't block responses
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# ✅ DO: Proper async patterns
|
|
80
|
+
from fastapi import BackgroundTasks
|
|
81
|
+
|
|
82
|
+
@router.post("/users/", response_model=UserResponse)
|
|
83
|
+
async def create_user_async(
|
|
84
|
+
user_data: CreateUserRequest,
|
|
85
|
+
background_tasks: BackgroundTasks,
|
|
86
|
+
db: AsyncConnection = Depends(get_async_db)
|
|
87
|
+
) -> UserResponse:
|
|
88
|
+
"""Create user with proper async patterns."""
|
|
89
|
+
|
|
90
|
+
# Main operation - blocking response
|
|
91
|
+
async with db.transaction():
|
|
92
|
+
user = await user_service.create_user_async(db, user_data)
|
|
93
|
+
|
|
94
|
+
# Non-critical operations in background (don't block response)
|
|
95
|
+
background_tasks.add_task(send_welcome_email, user.email)
|
|
96
|
+
background_tasks.add_task(update_analytics, "user_created")
|
|
97
|
+
|
|
98
|
+
return user
|
|
99
|
+
|
|
100
|
+
# ❌ REJECT: Mixed async/sync patterns
|
|
101
|
+
@router.post("/users/")
|
|
102
|
+
async def bad_async_patterns(user_data: dict):
|
|
103
|
+
# Blocking database call in async function
|
|
104
|
+
user = sync_db_connection.execute(f"INSERT INTO users...") # Blocks event loop
|
|
105
|
+
|
|
106
|
+
# Synchronous email sending that blocks response
|
|
107
|
+
email_client.send_email(user.email, "Welcome") # Should be background task
|
|
108
|
+
|
|
109
|
+
return {"status": "created"}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# ✅ DO: Proper FastAPI router with dependencies
|
|
114
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
115
|
+
from typing import List
|
|
116
|
+
|
|
117
|
+
router = APIRouter(prefix="/api/v1/users", tags=["users"])
|
|
118
|
+
|
|
119
|
+
async def get_db_connection():
|
|
120
|
+
async with database_pool.acquire() as conn:
|
|
121
|
+
try:
|
|
122
|
+
yield conn
|
|
123
|
+
finally:
|
|
124
|
+
# Connection automatically returned to pool
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
128
|
+
user = await auth_service.get_user_from_token(token)
|
|
129
|
+
if not user:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
132
|
+
detail="Invalid authentication credentials",
|
|
133
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
134
|
+
)
|
|
135
|
+
return user
|
|
136
|
+
|
|
137
|
+
@router.get("/{user_id}", response_model=UserResponse)
|
|
138
|
+
async def get_user(
|
|
139
|
+
user_id: int,
|
|
140
|
+
current_user: User = Depends(get_current_user),
|
|
141
|
+
db: AsyncConnection = Depends(get_db_connection)
|
|
142
|
+
):
|
|
143
|
+
if user_id != current_user.id and not current_user.is_admin:
|
|
144
|
+
raise HTTPException(
|
|
145
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
146
|
+
detail="Not authorized to access this user"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
user = await user_service.get_user(db, user_id)
|
|
150
|
+
if not user:
|
|
151
|
+
raise HTTPException(
|
|
152
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
153
|
+
detail="User not found"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return user
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Logging Standards:**
|
|
160
|
+
|
|
161
|
+
- **Appropriate log levels**: Use correct log levels for different types of messages
|
|
162
|
+
|
|
163
|
+
- DEBUG: Development/debugging information
|
|
164
|
+
- INFO: General operational information, successful operations
|
|
165
|
+
- WARNING: Potentially problematic situations that don't prevent operation
|
|
166
|
+
- ERROR: Error conditions that may still allow operation to continue
|
|
167
|
+
- CRITICAL: Serious errors that may prevent program from continuing
|
|
168
|
+
|
|
169
|
+
- **Context inclusion**: Include request IDs, user information, and operation context
|
|
170
|
+
- **Structured logging**: Use consistent log formats that can be parsed by log aggregation tools
|
|
171
|
+
- **No sensitive data**: Never log passwords, tokens, or personal information
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# ✅ DO: Proper logging levels and context
|
|
175
|
+
import logging
|
|
176
|
+
|
|
177
|
+
logger = logging.getLogger(__name__)
|
|
178
|
+
|
|
179
|
+
@router.post("/users/{user_id}/reset-password")
|
|
180
|
+
async def reset_password(user_id: int, request_id: str = Depends(get_request_id)):
|
|
181
|
+
"""Reset user password with proper logging."""
|
|
182
|
+
|
|
183
|
+
# INFO: Normal operation
|
|
184
|
+
logger.info(f"Password reset requested for user {user_id}", extra={
|
|
185
|
+
"request_id": request_id,
|
|
186
|
+
"user_id": user_id,
|
|
187
|
+
"operation": "password_reset"
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
await password_service.reset_password(user_id)
|
|
192
|
+
|
|
193
|
+
# INFO: Successful completion
|
|
194
|
+
logger.info(f"Password reset completed for user {user_id}", extra={
|
|
195
|
+
"request_id": request_id,
|
|
196
|
+
"user_id": user_id,
|
|
197
|
+
"status": "success"
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
except UserNotFoundError:
|
|
201
|
+
# WARNING: Expected error that doesn't prevent system operation
|
|
202
|
+
logger.warning(f"Password reset attempted for non-existent user {user_id}", extra={
|
|
203
|
+
"request_id": request_id,
|
|
204
|
+
"user_id": user_id,
|
|
205
|
+
"error_type": "user_not_found"
|
|
206
|
+
})
|
|
207
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
208
|
+
|
|
209
|
+
except DatabaseConnectionError as e:
|
|
210
|
+
# ERROR: Unexpected error that prevents operation but system can continue
|
|
211
|
+
logger.error(f"Database connection failed during password reset", extra={
|
|
212
|
+
"request_id": request_id,
|
|
213
|
+
"user_id": user_id,
|
|
214
|
+
"error": str(e),
|
|
215
|
+
"operation": "password_reset"
|
|
216
|
+
})
|
|
217
|
+
raise HTTPException(status_code=500, detail="Service temporarily unavailable")
|
|
218
|
+
|
|
219
|
+
# ❌ REJECT: Inappropriate log levels and missing context
|
|
220
|
+
@router.post("/users/login")
|
|
221
|
+
async def bad_logging_example(credentials: dict):
|
|
222
|
+
logger.error("User login attempted") # Should be INFO, not ERROR
|
|
223
|
+
|
|
224
|
+
if not credentials.get("username"):
|
|
225
|
+
logger.debug("Login failed - no username") # Should be WARNING with context
|
|
226
|
+
return {"error": "Bad request"}
|
|
227
|
+
|
|
228
|
+
logger.critical("Processing login") # Should be DEBUG or INFO, not CRITICAL
|
|
229
|
+
|
|
230
|
+
# No context, wrong levels, missing request tracking
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Phase 3: Server Testing Requirements
|
|
234
|
+
|
|
235
|
+
**API Testing Standards:**
|
|
236
|
+
|
|
237
|
+
- Use FastAPI's TestClient for endpoint testing
|
|
238
|
+
- Test all HTTP status codes (success, client errors, server errors)
|
|
239
|
+
- Test authentication and authorization scenarios
|
|
240
|
+
- Test input validation with invalid data
|
|
241
|
+
- Mock external dependencies in API tests
|
|
242
|
+
- Include integration tests with real database
|
|
243
|
+
|
|
244
|
+
**Request/Response Testing:**
|
|
245
|
+
|
|
246
|
+
- Test request body validation with Pydantic models
|
|
247
|
+
- Test query parameter validation
|
|
248
|
+
- Test response model serialization
|
|
249
|
+
- Test error response formats
|
|
250
|
+
- Test file upload functionality
|
|
251
|
+
- Include performance tests for API endpoints
|
|
252
|
+
|
|
253
|
+
### Phase 4: Performance and Scalability
|
|
254
|
+
|
|
255
|
+
**API Performance:**
|
|
256
|
+
|
|
257
|
+
- Use async/await for all I/O operations
|
|
258
|
+
- Implement proper database connection pooling
|
|
259
|
+
- Use response caching where appropriate
|
|
260
|
+
- Implement request batching for bulk operations
|
|
261
|
+
- Monitor API response times and error rates
|
|
262
|
+
|
|
263
|
+
**Middleware and Request Processing:**
|
|
264
|
+
|
|
265
|
+
- Implement request logging middleware with correlation IDs
|
|
266
|
+
- Use compression middleware for large responses
|
|
267
|
+
- Implement proper timeout handling for long-running operations
|
|
268
|
+
- Use background tasks for non-critical operations
|
|
269
|
+
- Monitor memory usage and connection counts
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# ✅ DO: Efficient async endpoint with proper error handling
|
|
273
|
+
@router.post("/users/bulk", response_model=List[UserResponse])
|
|
274
|
+
async def create_users_bulk(
|
|
275
|
+
users_data: List[CreateUserRequest],
|
|
276
|
+
background_tasks: BackgroundTasks,
|
|
277
|
+
db: AsyncConnection = Depends(get_db_connection),
|
|
278
|
+
current_user: User = Depends(get_admin_user)
|
|
279
|
+
):
|
|
280
|
+
if len(users_data) > 100: # Prevent abuse
|
|
281
|
+
raise HTTPException(
|
|
282
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
283
|
+
detail="Cannot create more than 100 users at once"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
# Batch operation for better performance
|
|
288
|
+
created_users = await user_service.create_users_batch(db, users_data)
|
|
289
|
+
|
|
290
|
+
# Non-critical operation in background
|
|
291
|
+
background_tasks.add_task(
|
|
292
|
+
send_welcome_emails,
|
|
293
|
+
[user.email for user in created_users]
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return created_users
|
|
297
|
+
|
|
298
|
+
except ValidationError as e:
|
|
299
|
+
raise HTTPException(
|
|
300
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
301
|
+
detail=f"Validation failed: {e}"
|
|
302
|
+
)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Bulk user creation failed: {e}", exc_info=True)
|
|
305
|
+
raise HTTPException(
|
|
306
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
307
|
+
detail="Internal server error"
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Phase 5: Server Maintainability
|
|
312
|
+
|
|
313
|
+
**API Documentation and Versioning:**
|
|
314
|
+
|
|
315
|
+
- Use OpenAPI tags for endpoint organization
|
|
316
|
+
- Document all endpoints with proper descriptions
|
|
317
|
+
- Implement API versioning strategy
|
|
318
|
+
- Use response examples in OpenAPI documentation
|
|
319
|
+
- Document all possible error responses
|
|
320
|
+
|
|
321
|
+
**Configuration and Environment:**
|
|
322
|
+
|
|
323
|
+
- Externalize all server configuration
|
|
324
|
+
- Use environment-specific settings
|
|
325
|
+
- Implement proper CORS configuration
|
|
326
|
+
- Configure security headers
|
|
327
|
+
- Document all configuration options
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Server-Specific Anti-Patterns
|
|
332
|
+
|
|
333
|
+
**Always Reject:**
|
|
334
|
+
|
|
335
|
+
- Endpoints without input validation
|
|
336
|
+
- Missing authentication on protected endpoints
|
|
337
|
+
- Raw dictionaries instead of Pydantic models
|
|
338
|
+
- Generic exception handling without proper HTTP responses
|
|
339
|
+
- Hardcoded configuration values
|
|
340
|
+
- Missing CORS configuration
|
|
341
|
+
- Endpoints without proper HTTP status codes
|
|
342
|
+
- Blocking operations in async endpoints
|
|
343
|
+
|
|
344
|
+
**Logging Anti-Patterns:**
|
|
345
|
+
|
|
346
|
+
- **Wrong log levels**: Using ERROR for normal operations, DEBUG for production warnings
|
|
347
|
+
- **Missing context**: Log messages without request IDs, user context, or operation details
|
|
348
|
+
- **Sensitive data**: Logging passwords, tokens, personal information
|
|
349
|
+
- **Inconsistent formats**: Different log formats that can't be parsed consistently
|
|
350
|
+
|
|
351
|
+
**Async Pattern Anti-Patterns:**
|
|
352
|
+
|
|
353
|
+
- **Blocking in async**: Using synchronous database calls or file operations in async endpoints
|
|
354
|
+
- **Missing background tasks**: Long-running operations that block API responses
|
|
355
|
+
- **Sync/async mixing**: Inconsistent use of async patterns within the same service
|
|
356
|
+
|
|
357
|
+
**Input Validation Anti-Patterns:**
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
# ❌ REJECT: No input validation
|
|
361
|
+
@app.post("/users/")
|
|
362
|
+
async def bad_endpoint(data: dict): # No validation
|
|
363
|
+
username = data["username"] # Could fail with KeyError
|
|
364
|
+
# No type checking, no sanitization
|
|
365
|
+
return {"status": "created"}
|
|
366
|
+
|
|
367
|
+
# ✅ REQUIRE: Proper validation with Pydantic
|
|
368
|
+
@app.post("/users/", response_model=UserResponse)
|
|
369
|
+
async def good_endpoint(user_data: CreateUserRequest):
|
|
370
|
+
# Pydantic automatically validates input
|
|
371
|
+
validated_user = await user_service.create_user(user_data)
|
|
372
|
+
return validated_user
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Error Handling Anti-Patterns:**
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
# ❌ REJECT: Poor error handling and logging
|
|
379
|
+
@app.get("/users/{user_id}")
|
|
380
|
+
async def bad_get_user(user_id: int):
|
|
381
|
+
logger.error(f"Getting user {user_id}") # Wrong log level
|
|
382
|
+
try:
|
|
383
|
+
user = await user_service.get_user(user_id)
|
|
384
|
+
return user # No response model
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.debug(f"User lookup failed: {e}") # Should be ERROR with context
|
|
387
|
+
return {"error": str(e)} # Wrong HTTP status, exposes internals
|
|
388
|
+
|
|
389
|
+
# ✅ REQUIRE: Proper error handling and logging
|
|
390
|
+
@app.get("/users/{user_id}", response_model=UserResponse)
|
|
391
|
+
async def good_get_user(user_id: int, request_id: str = Depends(get_request_id)):
|
|
392
|
+
logger.info(f"Retrieving user {user_id}", extra={"request_id": request_id})
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
user = await user_service.get_user(user_id)
|
|
396
|
+
if not user:
|
|
397
|
+
logger.warning(f"User {user_id} not found", extra={
|
|
398
|
+
"request_id": request_id,
|
|
399
|
+
"user_id": user_id
|
|
400
|
+
})
|
|
401
|
+
raise HTTPException(
|
|
402
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
403
|
+
detail="User not found"
|
|
404
|
+
)
|
|
405
|
+
return user
|
|
406
|
+
except ValidationError as e:
|
|
407
|
+
logger.warning(f"Invalid user ID format: {user_id}", extra={
|
|
408
|
+
"request_id": request_id,
|
|
409
|
+
"error": str(e)
|
|
410
|
+
})
|
|
411
|
+
raise HTTPException(
|
|
412
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
413
|
+
detail=f"Invalid request: {e}"
|
|
414
|
+
)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"User retrieval failed for {user_id}: {e}", extra={
|
|
417
|
+
"request_id": request_id,
|
|
418
|
+
"user_id": user_id
|
|
419
|
+
}, exc_info=True)
|
|
420
|
+
raise HTTPException(
|
|
421
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
422
|
+
detail="Internal server error"
|
|
423
|
+
)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Educational Context for Server Reviews
|
|
427
|
+
|
|
428
|
+
When reviewing server code, emphasize:
|
|
429
|
+
|
|
430
|
+
1. **Security Impact**: "API endpoints are the primary attack surface. Proper input validation and authentication aren't just good practices - they're essential for preventing data breaches and unauthorized access."
|
|
431
|
+
|
|
432
|
+
2. **Performance Impact**: "Server performance directly affects user experience. Blocking operations in async endpoints can cause cascading slowdowns that affect all API users."
|
|
433
|
+
|
|
434
|
+
3. **Reliability Impact**: "Proper error handling in APIs determines whether clients can gracefully handle failures or crash unexpectedly. Clear error responses help clients implement proper retry logic."
|
|
435
|
+
|
|
436
|
+
4. **Maintainability Impact**: "Well-structured FastAPI applications with proper dependency injection and router organization make it easier for teams to add features and maintain the codebase as it grows."
|
|
437
|
+
|
|
438
|
+
5. **Observability Impact**: "API logging and monitoring are critical for debugging production issues. Proper request correlation IDs and structured logging make the difference between quick problem resolution and extended outages."
|
|
439
|
+
|
|
440
|
+
6. **Async Pattern Impact**: "Proper async patterns are essential for handling concurrent requests efficiently. Blocking operations in async code can degrade performance for all users and cause connection pool exhaustion."
|
|
441
|
+
|
|
442
|
+
7. **Logging Quality Impact**: "Appropriate log levels and structured context are crucial for operational visibility. Wrong log levels create noise and hide real issues, while missing context makes debugging nearly impossible."
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import time
|
|
2
3
|
from typing import Any, Callable, List, Optional, Type
|
|
3
4
|
|
|
@@ -32,6 +33,7 @@ from application_sdk.server import ServerInterface
|
|
|
32
33
|
from application_sdk.server.fastapi.middleware.logmiddleware import LogMiddleware
|
|
33
34
|
from application_sdk.server.fastapi.middleware.metrics import MetricsMiddleware
|
|
34
35
|
from application_sdk.server.fastapi.models import (
|
|
36
|
+
ConfigMapResponse,
|
|
35
37
|
EventWorkflowRequest,
|
|
36
38
|
EventWorkflowResponse,
|
|
37
39
|
EventWorkflowTrigger,
|
|
@@ -95,6 +97,8 @@ class APIServer(ServerInterface):
|
|
|
95
97
|
docs_directory_path: str = "docs"
|
|
96
98
|
docs_export_path: str = "dist"
|
|
97
99
|
|
|
100
|
+
frontend_assets_path: str = "frontend/static"
|
|
101
|
+
|
|
98
102
|
workflows: List[WorkflowInterface] = []
|
|
99
103
|
event_triggers: List[EventWorkflowTrigger] = []
|
|
100
104
|
|
|
@@ -107,6 +111,7 @@ class APIServer(ServerInterface):
|
|
|
107
111
|
workflow_client: Optional[WorkflowClient] = None,
|
|
108
112
|
frontend_templates_path: str = "frontend/templates",
|
|
109
113
|
ui_enabled: bool = True,
|
|
114
|
+
has_configmap: bool = False,
|
|
110
115
|
):
|
|
111
116
|
"""Initialize the FastAPI application.
|
|
112
117
|
|
|
@@ -121,6 +126,7 @@ class APIServer(ServerInterface):
|
|
|
121
126
|
self.templates = Jinja2Templates(directory=frontend_templates_path)
|
|
122
127
|
self.duckdb_ui = DuckDBUI()
|
|
123
128
|
self.ui_enabled = ui_enabled
|
|
129
|
+
self.has_configmap = has_configmap
|
|
124
130
|
|
|
125
131
|
# Create the FastAPI app using the renamed import
|
|
126
132
|
if isinstance(lifespan, Callable):
|
|
@@ -177,6 +183,20 @@ class APIServer(ServerInterface):
|
|
|
177
183
|
except Exception as e:
|
|
178
184
|
logger.warning(str(e))
|
|
179
185
|
|
|
186
|
+
def frontend_home(self, request: Request) -> HTMLResponse:
|
|
187
|
+
frontend_html_path = os.path.join(
|
|
188
|
+
self.frontend_assets_path,
|
|
189
|
+
"index.html",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not os.path.exists(frontend_html_path) or not self.has_configmap:
|
|
193
|
+
return self.fallback_home(request)
|
|
194
|
+
|
|
195
|
+
with open(frontend_html_path, "r", encoding="utf-8") as file:
|
|
196
|
+
contents = file.read()
|
|
197
|
+
|
|
198
|
+
return HTMLResponse(content=contents)
|
|
199
|
+
|
|
180
200
|
def register_routers(self):
|
|
181
201
|
"""Register all routers with the FastAPI application.
|
|
182
202
|
|
|
@@ -195,7 +215,7 @@ class APIServer(ServerInterface):
|
|
|
195
215
|
self.app.include_router(self.dapr_router, prefix="/dapr")
|
|
196
216
|
self.app.include_router(self.events_router, prefix="/events/v1")
|
|
197
217
|
|
|
198
|
-
|
|
218
|
+
def fallback_home(self, request: Request) -> HTMLResponse:
|
|
199
219
|
return self.templates.TemplateResponse(
|
|
200
220
|
"index.html",
|
|
201
221
|
{
|
|
@@ -328,7 +348,6 @@ class APIServer(ServerInterface):
|
|
|
328
348
|
methods=["GET"],
|
|
329
349
|
response_class=RedirectResponse,
|
|
330
350
|
)
|
|
331
|
-
|
|
332
351
|
self.workflow_router.add_api_route(
|
|
333
352
|
"/auth",
|
|
334
353
|
self.test_auth,
|
|
@@ -374,6 +393,13 @@ class APIServer(ServerInterface):
|
|
|
374
393
|
methods=["POST"],
|
|
375
394
|
)
|
|
376
395
|
|
|
396
|
+
self.workflow_router.add_api_route(
|
|
397
|
+
"/configmap/{config_map_id}",
|
|
398
|
+
self.get_configmap,
|
|
399
|
+
methods=["GET"],
|
|
400
|
+
response_model=ConfigMapResponse,
|
|
401
|
+
)
|
|
402
|
+
|
|
377
403
|
self.dapr_router.add_api_route(
|
|
378
404
|
"/subscribe",
|
|
379
405
|
self.get_dapr_subscriptions,
|
|
@@ -390,7 +416,8 @@ class APIServer(ServerInterface):
|
|
|
390
416
|
|
|
391
417
|
def register_ui_routes(self):
|
|
392
418
|
"""Register the UI routes for the FastAPI application."""
|
|
393
|
-
self.app.get("/")(self.
|
|
419
|
+
self.app.get("/")(self.frontend_home)
|
|
420
|
+
|
|
394
421
|
# Mount static files
|
|
395
422
|
self.app.mount("/", StaticFiles(directory="frontend/static"), name="static")
|
|
396
423
|
|
|
@@ -587,6 +614,35 @@ class APIServer(ServerInterface):
|
|
|
587
614
|
)
|
|
588
615
|
raise e
|
|
589
616
|
|
|
617
|
+
async def get_configmap(self, config_map_id: str) -> ConfigMapResponse:
|
|
618
|
+
"""Get a configuration map by its ID.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
config_map_id (str): The ID of the configuration map to retrieve.
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
ConfigMapResponse: Response containing the configuration map.
|
|
625
|
+
"""
|
|
626
|
+
try:
|
|
627
|
+
if not self.handler:
|
|
628
|
+
raise Exception("Handler not initialized")
|
|
629
|
+
|
|
630
|
+
# Call the getConfigmap method on the workflow class
|
|
631
|
+
config_map_data = await self.handler.get_configmap(config_map_id)
|
|
632
|
+
|
|
633
|
+
return ConfigMapResponse(
|
|
634
|
+
success=True,
|
|
635
|
+
message="Configuration map fetched successfully",
|
|
636
|
+
data=config_map_data,
|
|
637
|
+
)
|
|
638
|
+
except Exception as e:
|
|
639
|
+
logger.error(f"Error fetching configuration map: {e}")
|
|
640
|
+
return ConfigMapResponse(
|
|
641
|
+
success=False,
|
|
642
|
+
message=f"Failed to fetch configuration map: {str(e)}",
|
|
643
|
+
data={},
|
|
644
|
+
)
|
|
645
|
+
|
|
590
646
|
async def get_workflow_config(
|
|
591
647
|
self, config_id: str, type: str = "workflows"
|
|
592
648
|
) -> WorkflowConfigResponse:
|
|
@@ -195,6 +195,33 @@ class WorkflowConfigResponse(BaseModel):
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
class ConfigMapResponse(BaseModel):
|
|
199
|
+
success: bool = Field(
|
|
200
|
+
..., description="Indicates whether the operation was successful"
|
|
201
|
+
)
|
|
202
|
+
message: str = Field(
|
|
203
|
+
..., description="Message describing the result of the operation"
|
|
204
|
+
)
|
|
205
|
+
data: Dict[str, Any] = Field(..., description="Configuration map object")
|
|
206
|
+
|
|
207
|
+
class Config:
|
|
208
|
+
schema_extra = {
|
|
209
|
+
"example": {
|
|
210
|
+
"success": True,
|
|
211
|
+
"message": "Configuration map fetched successfully",
|
|
212
|
+
"data": {
|
|
213
|
+
"config_map_id": "pikachu-config-001",
|
|
214
|
+
"name": "Pikachu Configuration",
|
|
215
|
+
"settings": {
|
|
216
|
+
"electric_type": True,
|
|
217
|
+
"level": 25,
|
|
218
|
+
"moves": ["Thunderbolt", "Quick Attack"],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
198
225
|
class WorkflowTrigger(BaseModel):
|
|
199
226
|
workflow_class: Optional[Type[WorkflowInterface]] = None
|
|
200
227
|
model_config = {"arbitrary_types_allowed": True}
|