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.
- application_sdk/activities/common/utils.py +78 -4
- application_sdk/activities/metadata_extraction/sql.py +400 -27
- application_sdk/application/__init__.py +2 -0
- application_sdk/application/metadata_extraction/sql.py +3 -0
- application_sdk/clients/models.py +42 -0
- application_sdk/clients/sql.py +17 -13
- application_sdk/common/aws_utils.py +259 -11
- application_sdk/common/utils.py +145 -9
- application_sdk/handlers/__init__.py +8 -1
- application_sdk/handlers/sql.py +63 -22
- application_sdk/inputs/__init__.py +98 -2
- application_sdk/inputs/json.py +59 -87
- application_sdk/inputs/parquet.py +173 -94
- application_sdk/observability/decorators/observability_decorator.py +36 -22
- application_sdk/server/fastapi/__init__.py +59 -3
- application_sdk/server/fastapi/models.py +27 -0
- application_sdk/test_utils/hypothesis/strategies/inputs/json_input.py +10 -5
- application_sdk/test_utils/hypothesis/strategies/inputs/parquet_input.py +9 -4
- application_sdk/version.py +1 -1
- {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/METADATA +1 -1
- {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/RECORD +24 -23
- {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-0.1.1rc40.dist-info → atlan_application_sdk-0.1.1rc42.dist-info}/licenses/LICENSE +0 -0
- {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:
|
|
26
|
-
chunk_size:
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
file_names (Optional[List[str]]
|
|
39
|
-
|
|
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
|
|
47
|
-
"""Read
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return pd.
|
|
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[
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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}
|
|
@@ -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=
|
|
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.
|
|
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
|
-
"
|
|
22
|
-
"file_names": file_names_strategy,
|
|
27
|
+
"file_names": st.one_of(st.none(), file_names_strategy),
|
|
23
28
|
}
|
|
24
29
|
)
|