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.
Files changed (35) hide show
  1. application_sdk/activities/.cursor/BUGBOT.md +424 -0
  2. application_sdk/activities/metadata_extraction/sql.py +400 -25
  3. application_sdk/application/__init__.py +2 -0
  4. application_sdk/application/metadata_extraction/sql.py +3 -0
  5. application_sdk/clients/.cursor/BUGBOT.md +280 -0
  6. application_sdk/clients/models.py +42 -0
  7. application_sdk/clients/sql.py +127 -87
  8. application_sdk/clients/temporal.py +3 -1
  9. application_sdk/common/.cursor/BUGBOT.md +316 -0
  10. application_sdk/common/aws_utils.py +259 -11
  11. application_sdk/common/utils.py +145 -9
  12. application_sdk/constants.py +8 -0
  13. application_sdk/decorators/.cursor/BUGBOT.md +279 -0
  14. application_sdk/handlers/__init__.py +8 -1
  15. application_sdk/handlers/sql.py +63 -22
  16. application_sdk/inputs/.cursor/BUGBOT.md +250 -0
  17. application_sdk/interceptors/.cursor/BUGBOT.md +320 -0
  18. application_sdk/interceptors/cleanup.py +171 -0
  19. application_sdk/interceptors/events.py +6 -6
  20. application_sdk/observability/decorators/observability_decorator.py +36 -22
  21. application_sdk/outputs/.cursor/BUGBOT.md +295 -0
  22. application_sdk/outputs/iceberg.py +4 -0
  23. application_sdk/outputs/json.py +6 -0
  24. application_sdk/outputs/parquet.py +13 -3
  25. application_sdk/server/.cursor/BUGBOT.md +442 -0
  26. application_sdk/server/fastapi/__init__.py +59 -3
  27. application_sdk/server/fastapi/models.py +27 -0
  28. application_sdk/services/objectstore.py +16 -3
  29. application_sdk/version.py +1 -1
  30. application_sdk/workflows/.cursor/BUGBOT.md +218 -0
  31. {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/METADATA +1 -1
  32. {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/RECORD +35 -24
  33. {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/WHEEL +0 -0
  34. {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/licenses/LICENSE +0 -0
  35. {atlan_application_sdk-0.1.1rc39.dist-info → atlan_application_sdk-0.1.1rc41.dist-info}/licenses/NOTICE +0 -0
@@ -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
- await ObjectStore.delete_prefix(
299
- prefix=get_object_store_prefix(self.output_path)
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
- async def home(self, request: Request) -> HTMLResponse:
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.home)
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}