atlan-application-sdk 0.1.1rc40__py3-none-any.whl → 0.1.1rc42__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 (24) hide show
  1. application_sdk/activities/common/utils.py +78 -4
  2. application_sdk/activities/metadata_extraction/sql.py +400 -27
  3. application_sdk/application/__init__.py +2 -0
  4. application_sdk/application/metadata_extraction/sql.py +3 -0
  5. application_sdk/clients/models.py +42 -0
  6. application_sdk/clients/sql.py +17 -13
  7. application_sdk/common/aws_utils.py +259 -11
  8. application_sdk/common/utils.py +145 -9
  9. application_sdk/handlers/__init__.py +8 -1
  10. application_sdk/handlers/sql.py +63 -22
  11. application_sdk/inputs/__init__.py +98 -2
  12. application_sdk/inputs/json.py +59 -87
  13. application_sdk/inputs/parquet.py +173 -94
  14. application_sdk/observability/decorators/observability_decorator.py +36 -22
  15. application_sdk/server/fastapi/__init__.py +59 -3
  16. application_sdk/server/fastapi/models.py +27 -0
  17. application_sdk/test_utils/hypothesis/strategies/inputs/json_input.py +10 -5
  18. application_sdk/test_utils/hypothesis/strategies/inputs/parquet_input.py +9 -4
  19. application_sdk/version.py +1 -1
  20. {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/METADATA +1 -1
  21. {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/RECORD +24 -23
  22. {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/WHEEL +0 -0
  23. {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/licenses/LICENSE +0 -0
  24. {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/licenses/NOTICE +0 -0
@@ -1,11 +1,7 @@
1
- import glob
2
- import os
3
1
  from typing import TYPE_CHECKING, AsyncIterator, Iterator, List, Optional, Union
4
2
 
5
- from application_sdk.activities.common.utils import get_object_store_prefix
6
3
  from application_sdk.inputs import Input
7
4
  from application_sdk.observability.logger_adaptor import get_logger
8
- from application_sdk.services.objectstore import ObjectStore
9
5
 
10
6
  logger = get_logger(__name__)
11
7
 
@@ -20,107 +16,139 @@ class ParquetInput(Input):
20
16
  Supports reading both single files and directories containing multiple parquet files.
21
17
  """
22
18
 
19
+ _EXTENSION = ".parquet"
20
+
23
21
  def __init__(
24
22
  self,
25
- path: Optional[str] = None,
26
- chunk_size: Optional[int] = 100000,
27
- input_prefix: Optional[str] = None,
23
+ path: str,
24
+ chunk_size: int = 100000,
28
25
  file_names: Optional[List[str]] = None,
29
26
  ):
30
27
  """Initialize the Parquet input class.
31
28
 
32
29
  Args:
33
30
  path (str): Path to parquet file or directory containing parquet files.
34
- chunk_size (Optional[int], optional): Number of rows per batch.
35
- Defaults to 100000.
36
- input_prefix (Optional[str], optional): Prefix for files when reading from object store.
37
- If provided, files will be read from object store. Defaults to None.
38
- file_names (Optional[List[str]], optional): List of file names to read.
39
- Defaults to None.
31
+ It accepts both types of paths:
32
+ local path or object store path
33
+ Wildcards are not supported.
34
+ chunk_size (int): Number of rows per batch. Defaults to 100000.
35
+ file_names (Optional[List[str]]): List of file names to read. Defaults to None.
36
+
37
+ Raises:
38
+ ValueError: When path is not provided or when single file path is combined with file_names
40
39
  """
40
+
41
+ # Validate that single file path and file_names are not both specified
42
+ if path.endswith(self._EXTENSION) and file_names:
43
+ raise ValueError(
44
+ f"Cannot specify both a single file path ('{path}') and file_names filter. "
45
+ f"Either provide a directory path with file_names, or specify the exact file path without file_names."
46
+ )
47
+
41
48
  self.path = path
42
49
  self.chunk_size = chunk_size
43
- self.input_prefix = input_prefix
44
50
  self.file_names = file_names
45
51
 
46
- async def download_files(self, local_path: str) -> Optional[str]:
47
- """Read a file from the object store.
48
-
49
- Args:
50
- local_path (str): Path to the local data in the temp directory.
52
+ async def get_dataframe(self) -> "pd.DataFrame":
53
+ """Read data from parquet file(s) and return as pandas DataFrame.
51
54
 
52
55
  Returns:
53
- Optional[str]: Path to the downloaded local file.
54
- """
55
- # if the path is a directory, then check if the directory has any parquet files
56
- parquet_files = []
57
- if os.path.isdir(local_path):
58
- parquet_files = glob.glob(os.path.join(local_path, "*.parquet"))
59
- else:
60
- parquet_files = glob.glob(local_path)
61
- if not parquet_files:
62
- if self.input_prefix:
63
- logger.info(
64
- f"Reading file from object store: {local_path} from {self.input_prefix}"
65
- )
66
- if os.path.isdir(local_path):
67
- await ObjectStore.download_prefix(
68
- source=get_object_store_prefix(local_path),
69
- destination=local_path,
70
- )
71
- else:
72
- await ObjectStore.download_file(
73
- source=get_object_store_prefix(local_path),
74
- destination=local_path,
75
- )
76
- else:
77
- raise ValueError(
78
- f"No parquet files found in {local_path} and no input prefix provided"
79
- )
56
+ pd.DataFrame: Combined dataframe from specified parquet files
80
57
 
81
- async def get_dataframe(self) -> "pd.DataFrame":
82
- """
83
- Method to read the data from the parquet file(s)
84
- and return as a single combined pandas dataframe.
58
+ Raises:
59
+ ValueError: When no valid path can be determined or no matching files found
60
+ Exception: When reading parquet files fails
85
61
 
86
- Returns:
87
- "pd.DataFrame": Combined dataframe from all parquet files.
62
+ Example transformation:
63
+ Input files:
64
+ +------------------+
65
+ | file1.parquet |
66
+ | file2.parquet |
67
+ | file3.parquet |
68
+ +------------------+
69
+
70
+ With file_names=["file1.parquet", "file3.parquet"]:
71
+ +-------+-------+-------+
72
+ | col1 | col2 | col3 |
73
+ +-------+-------+-------+
74
+ | val1 | val2 | val3 | # from file1.parquet
75
+ | val7 | val8 | val9 | # from file3.parquet
76
+ +-------+-------+-------+
77
+
78
+ Transformations:
79
+ - Only specified files are read and combined
80
+ - Column schemas must be compatible across files
81
+ - Only reads files in the specified directory
88
82
  """
89
83
  try:
90
84
  import pandas as pd
91
85
 
92
- path = self.path
93
- if self.input_prefix and self.path:
94
- path = await self.download_files(self.path)
95
- # Use pandas native read_parquet which can handle both single files and directories
96
- return pd.read_parquet(path)
86
+ # Ensure files are available (local or downloaded)
87
+ parquet_files = await self.download_files()
88
+ logger.info(f"Reading {len(parquet_files)} parquet files")
89
+
90
+ return pd.concat(
91
+ (pd.read_parquet(parquet_file) for parquet_file in parquet_files),
92
+ ignore_index=True,
93
+ )
97
94
  except Exception as e:
98
95
  logger.error(f"Error reading data from parquet file(s): {str(e)}")
99
- # Re-raise to match IcebergInput behavior
100
96
  raise
101
97
 
102
98
  async def get_batched_dataframe(
103
99
  self,
104
100
  ) -> Union[AsyncIterator["pd.DataFrame"], Iterator["pd.DataFrame"]]:
105
- """
106
- Method to read the data from the parquet file(s) in batches
107
- and return as an async iterator of pandas dataframes.
101
+ """Read data from parquet file(s) in batches as pandas DataFrames.
108
102
 
109
103
  Returns:
110
- AsyncIterator["pd.DataFrame"]: Async iterator of pandas dataframes.
104
+ AsyncIterator[pd.DataFrame]: Async iterator of pandas dataframes
105
+
106
+ Raises:
107
+ ValueError: When no parquet files found locally or in object store
108
+ Exception: When reading parquet files fails
109
+
110
+ Example transformation:
111
+ Input files:
112
+ +------------------+
113
+ | file1.parquet |
114
+ | file2.parquet |
115
+ | file3.parquet |
116
+ +------------------+
117
+
118
+ With file_names=["file1.parquet", "file2.parquet"] and chunk_size=2:
119
+ Batch 1:
120
+ +-------+-------+
121
+ | col1 | col2 |
122
+ +-------+-------+
123
+ | val1 | val2 | # from file1.parquet
124
+ | val3 | val4 | # from file1.parquet
125
+ +-------+-------+
126
+
127
+ Batch 2:
128
+ +-------+-------+
129
+ | col1 | col2 |
130
+ +-------+-------+
131
+ | val5 | val6 | # from file2.parquet
132
+ | val7 | val8 | # from file2.parquet
133
+ +-------+-------+
134
+
135
+ Transformations:
136
+ - Only specified files are combined then split into chunks
137
+ - Each batch is a separate DataFrame
138
+ - Only reads files in the specified directory
111
139
  """
112
140
  try:
113
141
  import pandas as pd
114
142
 
115
- path = self.path
116
- if self.input_prefix and self.path:
117
- path = await self.download_files(self.path)
118
- df = pd.read_parquet(path)
119
- if self.chunk_size:
143
+ # Ensure files are available (local or downloaded)
144
+ parquet_files = await self.download_files()
145
+ logger.info(f"Reading {len(parquet_files)} parquet files in batches")
146
+
147
+ # Process each file individually to maintain memory efficiency
148
+ for parquet_file in parquet_files:
149
+ df = pd.read_parquet(parquet_file)
120
150
  for i in range(0, len(df), self.chunk_size):
121
151
  yield df.iloc[i : i + self.chunk_size]
122
- else:
123
- yield df
124
152
  except Exception as e:
125
153
  logger.error(
126
154
  f"Error reading data from parquet file(s) in batches: {str(e)}"
@@ -128,51 +156,102 @@ class ParquetInput(Input):
128
156
  raise
129
157
 
130
158
  async def get_daft_dataframe(self) -> "daft.DataFrame": # noqa: F821
131
- """
132
- Method to read the data from the parquet file(s)
133
- and return as a single combined daft dataframe.
159
+ """Read data from parquet file(s) and return as daft DataFrame.
134
160
 
135
161
  Returns:
136
- daft.DataFrame: Combined daft dataframe from all parquet files.
162
+ daft.DataFrame: Combined daft dataframe from specified parquet files
163
+
164
+ Raises:
165
+ ValueError: When no parquet files found locally or in object store
166
+ Exception: When reading parquet files fails
167
+
168
+ Example transformation:
169
+ Input files:
170
+ +------------------+
171
+ | file1.parquet |
172
+ | file2.parquet |
173
+ | file3.parquet |
174
+ +------------------+
175
+
176
+ With file_names=["file1.parquet", "file3.parquet"]:
177
+ +-------+-------+-------+
178
+ | col1 | col2 | col3 |
179
+ +-------+-------+-------+
180
+ | val1 | val2 | val3 | # from file1.parquet
181
+ | val7 | val8 | val9 | # from file3.parquet
182
+ +-------+-------+-------+
183
+
184
+ Transformations:
185
+ - Only specified parquet files combined into single daft DataFrame
186
+ - Lazy evaluation for better performance
187
+ - Column schemas must be compatible across files
137
188
  """
138
189
  try:
139
190
  import daft # type: ignore
140
191
 
141
- if self.file_names:
142
- path = f"{self.path}/{self.file_names[0].split('/')[0]}"
143
- else:
144
- path = self.path
145
- if self.input_prefix and path:
146
- await self.download_files(path)
147
- return daft.read_parquet(f"{path}/*.parquet")
192
+ # Ensure files are available (local or downloaded)
193
+ parquet_files = await self.download_files()
194
+ logger.info(f"Reading {len(parquet_files)} parquet files with daft")
195
+
196
+ # Use the discovered/downloaded files directly
197
+ return daft.read_parquet(parquet_files)
148
198
  except Exception as e:
149
199
  logger.error(
150
200
  f"Error reading data from parquet file(s) using daft: {str(e)}"
151
201
  )
152
- # Re-raise to match IcebergInput behavior
153
202
  raise
154
203
 
155
204
  async def get_batched_daft_dataframe(self) -> AsyncIterator["daft.DataFrame"]: # type: ignore
156
- """
157
- Get batched daft dataframe from parquet file(s)
205
+ """Get batched daft dataframe from parquet file(s).
158
206
 
159
207
  Returns:
160
208
  AsyncIterator[daft.DataFrame]: An async iterator of daft DataFrames, each containing
161
- a batch of data from the parquet file(s).
209
+ a batch of data from individual parquet files
210
+
211
+ Raises:
212
+ ValueError: When no parquet files found locally or in object store
213
+ Exception: When reading parquet files fails
214
+
215
+ Example transformation:
216
+ Input files:
217
+ +------------------+
218
+ | file1.parquet |
219
+ | file2.parquet |
220
+ | file3.parquet |
221
+ +------------------+
222
+
223
+ With file_names=["file1.parquet", "file3.parquet"]:
224
+ Batch 1 (file1.parquet):
225
+ +-------+-------+
226
+ | col1 | col2 |
227
+ +-------+-------+
228
+ | val1 | val2 |
229
+ | val3 | val4 |
230
+ +-------+-------+
231
+
232
+ Batch 2 (file3.parquet):
233
+ +-------+-------+
234
+ | col1 | col2 |
235
+ +-------+-------+
236
+ | val7 | val8 |
237
+ | val9 | val10 |
238
+ +-------+-------+
239
+
240
+ Transformations:
241
+ - Each specified file becomes a separate daft DataFrame batch
242
+ - Lazy evaluation for better performance
243
+ - Files processed individually for memory efficiency
162
244
  """
163
245
  try:
164
246
  import daft # type: ignore
165
247
 
166
- if self.file_names:
167
- for file_name in self.file_names:
168
- path = f"{self.path}/{file_name}"
169
- if self.input_prefix and path:
170
- await self.download_files(path)
171
- yield daft.read_parquet(path)
172
- else:
173
- if self.path and self.input_prefix:
174
- await self.download_files(self.path)
175
- yield daft.read_parquet(f"{self.path}/*.parquet")
248
+ # Ensure files are available (local or downloaded)
249
+ parquet_files = await self.download_files()
250
+ logger.info(f"Reading {len(parquet_files)} parquet files as daft batches")
251
+
252
+ # Yield each discovered file as separate batch
253
+ for parquet_file in parquet_files:
254
+ yield daft.read_parquet(parquet_file)
176
255
 
177
256
  except Exception as error:
178
257
  logger.error(
@@ -4,7 +4,9 @@ import time
4
4
  import uuid
5
5
  from typing import Any, Callable, TypeVar, cast
6
6
 
7
- from application_sdk.observability.metrics_adaptor import MetricType
7
+ from application_sdk.observability.logger_adaptor import get_logger
8
+ from application_sdk.observability.metrics_adaptor import MetricType, get_metrics
9
+ from application_sdk.observability.traces_adaptor import get_traces
8
10
 
9
11
  T = TypeVar("T")
10
12
 
@@ -136,9 +138,9 @@ def _record_error_observability(
136
138
 
137
139
 
138
140
  def observability(
139
- logger: Any,
140
- metrics: Any,
141
- traces: Any,
141
+ logger: Any = None,
142
+ metrics: Any = None,
143
+ traces: Any = None,
142
144
  ) -> Callable[[Callable[..., T]], Callable[..., T]]:
143
145
  """Decorator for adding observability to functions.
144
146
 
@@ -146,16 +148,23 @@ def observability(
146
148
  It handles both synchronous and asynchronous functions.
147
149
 
148
150
  Args:
149
- logger: Logger instance for operation logging
150
- metrics: Metrics adapter for recording operation metrics
151
- traces: Traces adapter for recording operation traces
151
+ logger: Logger instance for operation logging. If None, auto-initializes using get_logger()
152
+ metrics: Metrics adapter for recording operation metrics. If None, auto-initializes using get_metrics()
153
+ traces: Traces adapter for recording operation traces. If None, auto-initializes using get_traces()
152
154
 
153
155
  Returns:
154
156
  Callable: Decorated function with observability
155
157
 
156
158
  Example:
157
159
  ```python
160
+ # With explicit observability components
158
161
  @observability(logger, metrics, traces)
162
+ async def my_function():
163
+ # Function implementation
164
+ pass
165
+
166
+ # With auto-initialization (recommended)
167
+ @observability()
159
168
  async def my_function():
160
169
  # Function implementation
161
170
  pass
@@ -163,6 +172,11 @@ def observability(
163
172
  """
164
173
 
165
174
  def decorator(func: Callable[..., T]) -> Callable[..., T]:
175
+ # Auto-initialize observability components if not provided
176
+ actual_logger = logger or get_logger(func.__module__)
177
+ actual_metrics = metrics or get_metrics()
178
+ actual_traces = traces or get_traces()
179
+
166
180
  # Get function metadata
167
181
  func_name = func.__name__
168
182
  func_doc = func.__doc__ or f"Executing {func_name}"
@@ -170,7 +184,7 @@ def observability(
170
184
  is_async = inspect.iscoroutinefunction(func)
171
185
 
172
186
  # Debug logging for function decoration
173
- logger.debug(f"Decorating function {func_name} (async={is_async})")
187
+ actual_logger.debug(f"Decorating function {func_name} (async={is_async})")
174
188
 
175
189
  @functools.wraps(func)
176
190
  async def async_wrapper(*args: Any, **kwargs: Any) -> T:
@@ -181,16 +195,16 @@ def observability(
181
195
 
182
196
  try:
183
197
  # Log start of operation
184
- logger.debug(f"Starting async function {func_name}")
198
+ actual_logger.debug(f"Starting async function {func_name}")
185
199
 
186
200
  # Execute the function
187
201
  result = await func(*args, **kwargs)
188
202
 
189
203
  # Record success observability
190
204
  _record_success_observability(
191
- logger,
192
- metrics,
193
- traces,
205
+ actual_logger,
206
+ actual_metrics,
207
+ actual_traces,
194
208
  func_name,
195
209
  func_doc,
196
210
  func_module,
@@ -204,9 +218,9 @@ def observability(
204
218
  except Exception as e:
205
219
  # Record error observability
206
220
  _record_error_observability(
207
- logger,
208
- metrics,
209
- traces,
221
+ actual_logger,
222
+ actual_metrics,
223
+ actual_traces,
210
224
  func_name,
211
225
  func_doc,
212
226
  func_module,
@@ -226,16 +240,16 @@ def observability(
226
240
 
227
241
  try:
228
242
  # Log start of operation
229
- logger.debug(f"Starting sync function {func_name}")
243
+ actual_logger.debug(f"Starting sync function {func_name}")
230
244
 
231
245
  # Execute the function
232
246
  result = func(*args, **kwargs)
233
247
 
234
248
  # Record success observability
235
249
  _record_success_observability(
236
- logger,
237
- metrics,
238
- traces,
250
+ actual_logger,
251
+ actual_metrics,
252
+ actual_traces,
239
253
  func_name,
240
254
  func_doc,
241
255
  func_module,
@@ -249,9 +263,9 @@ def observability(
249
263
  except Exception as e:
250
264
  # Record error observability
251
265
  _record_error_observability(
252
- logger,
253
- metrics,
254
- traces,
266
+ actual_logger,
267
+ actual_metrics,
268
+ actual_traces,
255
269
  func_name,
256
270
  func_doc,
257
271
  func_module,
@@ -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}
@@ -2,11 +2,17 @@ from hypothesis import strategies as st
2
2
 
3
3
  # Strategy for generating safe file path components
4
4
  safe_path_strategy = st.text(
5
- alphabet=st.characters(),
6
- )
5
+ alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-",
6
+ min_size=1,
7
+ max_size=20,
8
+ ).map(lambda x: f"/data/{x}")
7
9
 
8
10
  # Strategy for generating file names
9
- file_name_strategy = st.builds(lambda name: f"{name}.json", name=safe_path_strategy)
11
+ file_name_strategy = st.text(
12
+ alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-",
13
+ min_size=1,
14
+ max_size=10,
15
+ ).map(lambda x: f"{x}.json")
10
16
 
11
17
  # Strategy for generating lists of file names
12
18
  file_names_strategy = st.lists(file_name_strategy, unique=True)
@@ -18,7 +24,6 @@ download_prefix_strategy = safe_path_strategy
18
24
  json_input_config_strategy = st.fixed_dictionaries(
19
25
  {
20
26
  "path": safe_path_strategy,
21
- "download_file_prefix": download_prefix_strategy,
22
- "file_names": file_names_strategy,
27
+ "file_names": st.one_of(st.none(), file_names_strategy),
23
28
  }
24
29
  )