atlan-application-sdk 1.1.1__py3-none-any.whl → 2.1.0__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/sql_utils.py +312 -0
- application_sdk/activities/common/utils.py +1 -45
- application_sdk/activities/metadata_extraction/sql.py +110 -353
- application_sdk/activities/query_extraction/sql.py +12 -11
- application_sdk/application/__init__.py +1 -1
- application_sdk/clients/sql.py +167 -1
- application_sdk/clients/temporal.py +6 -6
- application_sdk/common/types.py +8 -0
- application_sdk/common/utils.py +1 -8
- application_sdk/constants.py +1 -1
- application_sdk/handlers/sql.py +10 -25
- application_sdk/interceptors/events.py +1 -1
- application_sdk/io/__init__.py +749 -0
- application_sdk/io/json.py +473 -0
- application_sdk/{outputs → io}/parquet.py +414 -47
- application_sdk/io/utils.py +307 -0
- application_sdk/observability/observability.py +16 -12
- application_sdk/server/fastapi/middleware/logmiddleware.py +23 -17
- application_sdk/server/fastapi/middleware/metrics.py +27 -24
- application_sdk/server/fastapi/models.py +1 -1
- application_sdk/server/fastapi/routers/server.py +1 -1
- application_sdk/server/fastapi/utils.py +10 -0
- application_sdk/services/eventstore.py +4 -4
- application_sdk/services/objectstore.py +14 -1
- application_sdk/services/secretstore.py +1 -1
- application_sdk/test_utils/hypothesis/strategies/outputs/json_output.py +0 -1
- application_sdk/test_utils/hypothesis/strategies/server/fastapi/__init__.py +1 -1
- application_sdk/version.py +1 -1
- application_sdk/worker.py +1 -1
- {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.1.0.dist-info}/METADATA +9 -11
- {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.1.0.dist-info}/RECORD +36 -43
- application_sdk/common/dataframe_utils.py +0 -42
- application_sdk/events/__init__.py +0 -5
- application_sdk/inputs/.cursor/BUGBOT.md +0 -250
- application_sdk/inputs/__init__.py +0 -168
- application_sdk/inputs/iceberg.py +0 -75
- application_sdk/inputs/json.py +0 -136
- application_sdk/inputs/parquet.py +0 -272
- application_sdk/inputs/sql_query.py +0 -271
- application_sdk/outputs/.cursor/BUGBOT.md +0 -295
- application_sdk/outputs/__init__.py +0 -453
- application_sdk/outputs/iceberg.py +0 -139
- application_sdk/outputs/json.py +0 -268
- /application_sdk/{events → interceptors}/models.py +0 -0
- /application_sdk/{common/dapr_utils.py → services/_utils.py} +0 -0
- {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.1.0.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.1.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import concurrent
|
|
3
|
-
from typing import TYPE_CHECKING, AsyncIterator, Iterator, Optional, Union
|
|
4
|
-
|
|
5
|
-
from application_sdk.inputs import Input
|
|
6
|
-
from application_sdk.observability.logger_adaptor import get_logger
|
|
7
|
-
|
|
8
|
-
logger = get_logger(__name__)
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
import daft
|
|
12
|
-
import pandas as pd
|
|
13
|
-
from sqlalchemy.engine import Engine
|
|
14
|
-
from sqlalchemy.orm import Session
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class SQLQueryInput(Input):
|
|
18
|
-
"""Input handler for SQL queries.
|
|
19
|
-
|
|
20
|
-
This class provides asynchronous functionality to execute SQL queries and return
|
|
21
|
-
results as DataFrames, with support for both pandas and daft formats.
|
|
22
|
-
|
|
23
|
-
Attributes:
|
|
24
|
-
query (str): The SQL query to execute.
|
|
25
|
-
engine (Union[Engine, str]): SQLAlchemy engine or connection string.
|
|
26
|
-
chunk_size (Optional[int]): Number of rows to fetch per batch.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
query: str
|
|
30
|
-
engine: Union["Engine", str]
|
|
31
|
-
chunk_size: Optional[int]
|
|
32
|
-
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
query: str,
|
|
36
|
-
engine: Union["Engine", str],
|
|
37
|
-
chunk_size: Optional[int] = 5000,
|
|
38
|
-
):
|
|
39
|
-
"""Initialize the async SQL query input handler.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
engine (Union[Engine, str]): SQLAlchemy engine or connection string.
|
|
43
|
-
query (str): The SQL query to execute.
|
|
44
|
-
chunk_size (Optional[int], optional): Number of rows per batch.
|
|
45
|
-
Defaults to 5000.
|
|
46
|
-
"""
|
|
47
|
-
self.query = query
|
|
48
|
-
self.engine = engine
|
|
49
|
-
self.chunk_size = chunk_size
|
|
50
|
-
self.engine = engine
|
|
51
|
-
|
|
52
|
-
def _execute_pandas_query(
|
|
53
|
-
self, conn
|
|
54
|
-
) -> Union["pd.DataFrame", Iterator["pd.DataFrame"]]:
|
|
55
|
-
"""Helper function to execute SQL query using pandas.
|
|
56
|
-
The function is responsible for using import_optional_dependency method of the pandas library to import sqlalchemy
|
|
57
|
-
This function helps pandas in determining weather to use the sqlalchemy connection object and constructs like text()
|
|
58
|
-
or use the underlying database connection object. This has been done to make sure connectors like the Redshift connector,
|
|
59
|
-
which do not support the sqlalchemy connection object, can be made compatible with the application-sdk.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
conn: Database connection object.
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
Union["pd.DataFrame", Iterator["pd.DataFrame"]]: Query results as DataFrame
|
|
66
|
-
or iterator of DataFrames if chunked.
|
|
67
|
-
"""
|
|
68
|
-
import pandas as pd
|
|
69
|
-
from pandas.compat._optional import import_optional_dependency
|
|
70
|
-
from sqlalchemy import text
|
|
71
|
-
|
|
72
|
-
if import_optional_dependency("sqlalchemy", errors="ignore"):
|
|
73
|
-
return pd.read_sql_query(text(self.query), conn, chunksize=self.chunk_size)
|
|
74
|
-
else:
|
|
75
|
-
dbapi_conn = getattr(conn, "connection", None)
|
|
76
|
-
return pd.read_sql_query(self.query, dbapi_conn, chunksize=self.chunk_size)
|
|
77
|
-
|
|
78
|
-
def _read_sql_query(
|
|
79
|
-
self, session: "Session"
|
|
80
|
-
) -> Union["pd.DataFrame", Iterator["pd.DataFrame"]]:
|
|
81
|
-
"""Execute SQL query using the provided session.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
session: SQLAlchemy session for database operations.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
Union["pd.DataFrame", Iterator["pd.DataFrame"]]: Query results as DataFrame
|
|
88
|
-
or iterator of DataFrames if chunked.
|
|
89
|
-
"""
|
|
90
|
-
conn = session.connection()
|
|
91
|
-
return self._execute_pandas_query(conn)
|
|
92
|
-
|
|
93
|
-
def _execute_query_daft(
|
|
94
|
-
self,
|
|
95
|
-
) -> Union["daft.DataFrame", Iterator["daft.DataFrame"]]:
|
|
96
|
-
"""Execute SQL query using the provided engine and daft.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
Union["daft.DataFrame", Iterator["daft.DataFrame"]]: Query results as DataFrame
|
|
100
|
-
or iterator of DataFrames if chunked.
|
|
101
|
-
"""
|
|
102
|
-
# Daft uses ConnectorX to read data from SQL by default for supported connectors
|
|
103
|
-
# If a connection string is passed, it will use ConnectorX to read data
|
|
104
|
-
# For unsupported connectors and if directly engine is passed, it will use SQLAlchemy
|
|
105
|
-
import daft
|
|
106
|
-
|
|
107
|
-
if isinstance(self.engine, str):
|
|
108
|
-
return daft.read_sql(
|
|
109
|
-
self.query, self.engine, infer_schema_length=self.chunk_size
|
|
110
|
-
)
|
|
111
|
-
return daft.read_sql(
|
|
112
|
-
self.query, self.engine.connect, infer_schema_length=self.chunk_size
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
def _execute_query(self) -> Union["pd.DataFrame", Iterator["pd.DataFrame"]]:
|
|
116
|
-
"""Execute SQL query using the provided engine and pandas.
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
Union["pd.DataFrame", Iterator["pd.DataFrame"]]: Query results as DataFrame
|
|
120
|
-
or iterator of DataFrames if chunked.
|
|
121
|
-
"""
|
|
122
|
-
with self.engine.connect() as conn:
|
|
123
|
-
return self._execute_pandas_query(conn)
|
|
124
|
-
|
|
125
|
-
async def get_batched_dataframe(
|
|
126
|
-
self,
|
|
127
|
-
) -> Union[AsyncIterator["pd.DataFrame"], Iterator["pd.DataFrame"]]: # type: ignore
|
|
128
|
-
"""Get query results as batched pandas DataFrames asynchronously.
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
AsyncIterator["pd.DataFrame"]: Async iterator yielding batches of query results.
|
|
132
|
-
|
|
133
|
-
Raises:
|
|
134
|
-
ValueError: If engine is a string instead of SQLAlchemy engine.
|
|
135
|
-
Exception: If there's an error executing the query.
|
|
136
|
-
"""
|
|
137
|
-
try:
|
|
138
|
-
if isinstance(self.engine, str):
|
|
139
|
-
raise ValueError("Engine should be an SQLAlchemy engine object")
|
|
140
|
-
|
|
141
|
-
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
142
|
-
|
|
143
|
-
async_session = None
|
|
144
|
-
if self.engine and isinstance(self.engine, AsyncEngine):
|
|
145
|
-
from sqlalchemy.orm import sessionmaker
|
|
146
|
-
|
|
147
|
-
async_session = sessionmaker(
|
|
148
|
-
self.engine, expire_on_commit=False, class_=AsyncSession
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
if async_session:
|
|
152
|
-
async with async_session() as session:
|
|
153
|
-
return await session.run_sync(self._read_sql_query)
|
|
154
|
-
else:
|
|
155
|
-
# Run the blocking operation in a thread pool
|
|
156
|
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
157
|
-
return await asyncio.get_event_loop().run_in_executor( # type: ignore
|
|
158
|
-
executor, self._execute_query
|
|
159
|
-
)
|
|
160
|
-
except Exception as e:
|
|
161
|
-
logger.error(f"Error reading batched data(pandas) from SQL: {str(e)}")
|
|
162
|
-
|
|
163
|
-
async def get_dataframe(self) -> "pd.DataFrame":
|
|
164
|
-
"""Get all query results as a single pandas DataFrame asynchronously.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
pd.DataFrame: Query results as a DataFrame.
|
|
168
|
-
|
|
169
|
-
Raises:
|
|
170
|
-
ValueError: If engine is a string instead of SQLAlchemy engine.
|
|
171
|
-
Exception: If there's an error executing the query.
|
|
172
|
-
"""
|
|
173
|
-
try:
|
|
174
|
-
if isinstance(self.engine, str):
|
|
175
|
-
raise ValueError("Engine should be an SQLAlchemy engine object")
|
|
176
|
-
|
|
177
|
-
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
178
|
-
|
|
179
|
-
async_session = None
|
|
180
|
-
if self.engine and isinstance(self.engine, AsyncEngine):
|
|
181
|
-
from sqlalchemy.orm import sessionmaker
|
|
182
|
-
|
|
183
|
-
async_session = sessionmaker(
|
|
184
|
-
self.engine, expire_on_commit=False, class_=AsyncSession
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
if async_session:
|
|
188
|
-
async with async_session() as session:
|
|
189
|
-
return await session.run_sync(self._read_sql_query)
|
|
190
|
-
else:
|
|
191
|
-
# Run the blocking operation in a thread pool
|
|
192
|
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
193
|
-
result = await asyncio.get_event_loop().run_in_executor(
|
|
194
|
-
executor, self._execute_query
|
|
195
|
-
)
|
|
196
|
-
import pandas as pd
|
|
197
|
-
|
|
198
|
-
if isinstance(result, pd.DataFrame):
|
|
199
|
-
return result
|
|
200
|
-
raise Exception(
|
|
201
|
-
"Unable to get pandas dataframe from SQL query results"
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
except Exception as e:
|
|
205
|
-
logger.error(f"Error reading data(pandas) from SQL: {str(e)}")
|
|
206
|
-
raise e
|
|
207
|
-
|
|
208
|
-
async def get_daft_dataframe(self) -> "daft.DataFrame": # noqa: F821
|
|
209
|
-
"""Get query results as a daft DataFrame.
|
|
210
|
-
|
|
211
|
-
This method uses ConnectorX to read data from SQL for supported connectors.
|
|
212
|
-
For unsupported connectors and direct engine usage, it falls back to SQLAlchemy.
|
|
213
|
-
|
|
214
|
-
Returns:
|
|
215
|
-
daft.DataFrame: Query results as a daft DataFrame.
|
|
216
|
-
|
|
217
|
-
Raises:
|
|
218
|
-
ValueError: If engine is a string instead of SQLAlchemy engine.
|
|
219
|
-
Exception: If there's an error executing the query.
|
|
220
|
-
|
|
221
|
-
Note:
|
|
222
|
-
For ConnectorX supported sources, see:
|
|
223
|
-
https://sfu-db.github.io/connector-x/intro.html#sources
|
|
224
|
-
"""
|
|
225
|
-
try:
|
|
226
|
-
import daft
|
|
227
|
-
|
|
228
|
-
# Run the blocking operation in a thread pool
|
|
229
|
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
230
|
-
result = await asyncio.get_event_loop().run_in_executor(
|
|
231
|
-
executor, self._execute_query_daft
|
|
232
|
-
)
|
|
233
|
-
if isinstance(result, daft.DataFrame):
|
|
234
|
-
return result
|
|
235
|
-
raise
|
|
236
|
-
except Exception as e:
|
|
237
|
-
logger.error(f"Error reading data(daft) from SQL: {str(e)}")
|
|
238
|
-
raise
|
|
239
|
-
|
|
240
|
-
async def get_batched_daft_dataframe(
|
|
241
|
-
self,
|
|
242
|
-
) -> Union[AsyncIterator["daft.DataFrame"], Iterator["daft.DataFrame"]]: # noqa: F821
|
|
243
|
-
"""Get query results as batched daft DataFrames.
|
|
244
|
-
|
|
245
|
-
This method reads data using pandas in batches since daft does not support
|
|
246
|
-
batch reading. Each pandas DataFrame is then converted to a daft DataFrame.
|
|
247
|
-
|
|
248
|
-
Returns:
|
|
249
|
-
AsyncIterator[daft.DataFrame]: Async iterator yielding batches of query results
|
|
250
|
-
as daft DataFrames.
|
|
251
|
-
|
|
252
|
-
Raises:
|
|
253
|
-
ValueError: If engine is a string instead of SQLAlchemy engine.
|
|
254
|
-
Exception: If there's an error executing the query.
|
|
255
|
-
|
|
256
|
-
Note:
|
|
257
|
-
This method uses pandas for batch reading since daft does not support
|
|
258
|
-
reading data in batches natively.
|
|
259
|
-
"""
|
|
260
|
-
try:
|
|
261
|
-
import daft
|
|
262
|
-
|
|
263
|
-
if isinstance(self.engine, str):
|
|
264
|
-
raise ValueError("Engine should be an SQLAlchemy engine object")
|
|
265
|
-
|
|
266
|
-
# Use async for to consume the AsyncIterator properly
|
|
267
|
-
async for dataframe in self.get_batched_dataframe():
|
|
268
|
-
daft_dataframe = daft.from_pandas(dataframe)
|
|
269
|
-
yield daft_dataframe
|
|
270
|
-
except Exception as e:
|
|
271
|
-
logger.error(f"Error reading batched data(daft) from SQL: {str(e)}")
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
# Output Code Review Guidelines - Data Output Processing
|
|
2
|
-
|
|
3
|
-
## Context-Specific Patterns
|
|
4
|
-
|
|
5
|
-
This directory contains output processing implementations for various data formats (JSON, Parquet, Iceberg). Output processors must handle data uploads efficiently while maintaining data integrity and correct destination paths.
|
|
6
|
-
|
|
7
|
-
### Phase 1: Critical Output Safety Issues
|
|
8
|
-
|
|
9
|
-
**Object Store Path Management:**
|
|
10
|
-
|
|
11
|
-
- **Correct destination paths**: Upload paths must respect user-configured output prefixes
|
|
12
|
-
- **Path construction accuracy**: Object store keys must be calculated correctly, not hardcoded
|
|
13
|
-
- **User prefix preservation**: Respect user-provided output directories and naming conventions
|
|
14
|
-
- **Path validation**: Ensure upload paths don't conflict with existing data
|
|
15
|
-
|
|
16
|
-
**Data Integrity and Security:**
|
|
17
|
-
|
|
18
|
-
- All output data must be validated before upload
|
|
19
|
-
- File permissions and access controls must be properly set
|
|
20
|
-
- Data serialization must be consistent and recoverable
|
|
21
|
-
- Prevent overwriting critical data without confirmation
|
|
22
|
-
- Maintain data lineage information in output metadata
|
|
23
|
-
|
|
24
|
-
```python
|
|
25
|
-
# ✅ DO: Proper object store upload path handling
|
|
26
|
-
class JsonOutput:
|
|
27
|
-
async def upload_to_object_store(
|
|
28
|
-
self,
|
|
29
|
-
data: List[dict],
|
|
30
|
-
output_prefix: str, # User-provided output location
|
|
31
|
-
filename: str
|
|
32
|
-
) -> dict:
|
|
33
|
-
"""Upload data with correct path handling."""
|
|
34
|
-
|
|
35
|
-
# Construct full object store path respecting user's output prefix
|
|
36
|
-
object_store_key = os.path.join(output_prefix, filename)
|
|
37
|
-
|
|
38
|
-
# Serialize data
|
|
39
|
-
json_data = orjson.dumps(data, option=orjson.OPT_APPEND_NEWLINE)
|
|
40
|
-
|
|
41
|
-
# Upload to correct location
|
|
42
|
-
result = await self.object_store.upload_file(
|
|
43
|
-
data=json_data,
|
|
44
|
-
destination=object_store_key # Respect user's intended location
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
return result
|
|
48
|
-
|
|
49
|
-
# ❌ REJECT: Incorrect path handling
|
|
50
|
-
class BadJsonOutput:
|
|
51
|
-
async def upload_to_object_store(self, data: List[dict], filename: str):
|
|
52
|
-
# Wrong: hardcoded or derived path, ignoring user configuration
|
|
53
|
-
object_store_key = get_object_store_prefix(f"/tmp/{filename}") # Ignores output_prefix!
|
|
54
|
-
|
|
55
|
-
result = await self.object_store.upload_file(
|
|
56
|
-
data=orjson.dumps(data),
|
|
57
|
-
destination=object_store_key # Wrong destination!
|
|
58
|
-
)
|
|
59
|
-
return result
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Phase 2: Output Architecture Patterns
|
|
63
|
-
|
|
64
|
-
**Performance Optimization Requirements:**
|
|
65
|
-
|
|
66
|
-
- **Parallelization opportunities**: Flag sequential upload operations that could be parallelized
|
|
67
|
-
- **Batch processing**: Group related uploads to reduce overhead
|
|
68
|
-
- **Streaming uploads**: Use streaming for large datasets instead of loading into memory
|
|
69
|
-
- **Connection optimization**: Reuse object store connections across operations
|
|
70
|
-
|
|
71
|
-
**Resource Management:**
|
|
72
|
-
|
|
73
|
-
- Use proper connection pooling for object store operations
|
|
74
|
-
- Implement timeout handling for upload operations
|
|
75
|
-
- Clean up temporary files after upload
|
|
76
|
-
- Handle partial upload failures gracefully
|
|
77
|
-
- Monitor memory usage during large data serialization
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
# ✅ DO: Parallel upload processing
|
|
81
|
-
async def upload_multiple_datasets_parallel(
|
|
82
|
-
self,
|
|
83
|
-
datasets: List[Tuple[List[dict], str]], # (data, filename) pairs
|
|
84
|
-
output_prefix: str
|
|
85
|
-
) -> List[dict]:
|
|
86
|
-
"""Upload multiple datasets in parallel for better performance."""
|
|
87
|
-
|
|
88
|
-
async def upload_single_dataset(data: List[dict], filename: str) -> dict:
|
|
89
|
-
"""Upload a single dataset with error handling."""
|
|
90
|
-
try:
|
|
91
|
-
object_store_key = os.path.join(output_prefix, filename)
|
|
92
|
-
serialized_data = orjson.dumps(data, option=orjson.OPT_APPEND_NEWLINE)
|
|
93
|
-
|
|
94
|
-
return await self.object_store.upload_file(
|
|
95
|
-
data=serialized_data,
|
|
96
|
-
destination=object_store_key
|
|
97
|
-
)
|
|
98
|
-
except Exception as e:
|
|
99
|
-
logger.error(f"Failed to upload {filename}: {e}")
|
|
100
|
-
raise
|
|
101
|
-
|
|
102
|
-
# Parallel processing with controlled concurrency
|
|
103
|
-
semaphore = asyncio.Semaphore(5) # Limit concurrent uploads
|
|
104
|
-
|
|
105
|
-
async def upload_with_semaphore(data: List[dict], filename: str) -> dict:
|
|
106
|
-
async with semaphore:
|
|
107
|
-
return await upload_single_dataset(data, filename)
|
|
108
|
-
|
|
109
|
-
tasks = [upload_with_semaphore(data, filename) for data, filename in datasets]
|
|
110
|
-
return await asyncio.gather(*tasks)
|
|
111
|
-
|
|
112
|
-
# ❌ REJECT: Sequential upload processing
|
|
113
|
-
async def upload_multiple_datasets_sequential(
|
|
114
|
-
self,
|
|
115
|
-
datasets: List[Tuple[List[dict], str]],
|
|
116
|
-
output_prefix: str
|
|
117
|
-
) -> List[dict]:
|
|
118
|
-
"""Sequential uploads - should be flagged for parallelization."""
|
|
119
|
-
results = []
|
|
120
|
-
for data, filename in datasets: # FLAG: Could be parallelized
|
|
121
|
-
object_store_key = os.path.join(output_prefix, filename)
|
|
122
|
-
result = await self.object_store.upload_file(data, object_store_key)
|
|
123
|
-
results.append(result)
|
|
124
|
-
return results
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Phase 3: Output Testing Requirements
|
|
128
|
-
|
|
129
|
-
**Data Output Testing:**
|
|
130
|
-
|
|
131
|
-
- Test with various data formats and sizes
|
|
132
|
-
- Test serialization and deserialization consistency
|
|
133
|
-
- Test partial upload scenarios and recovery
|
|
134
|
-
- Mock object store operations in unit tests
|
|
135
|
-
- Include integration tests with real object store
|
|
136
|
-
- Test data corruption detection and prevention
|
|
137
|
-
|
|
138
|
-
**Performance Testing:**
|
|
139
|
-
|
|
140
|
-
- Include tests for large dataset uploads
|
|
141
|
-
- Test memory usage during serialization
|
|
142
|
-
- Test concurrent upload operations
|
|
143
|
-
- Verify timeout handling works correctly
|
|
144
|
-
- Test connection pool behavior under load
|
|
145
|
-
|
|
146
|
-
### Phase 4: Performance and Scalability
|
|
147
|
-
|
|
148
|
-
**Data Upload Efficiency:**
|
|
149
|
-
|
|
150
|
-
- Use streaming uploads for large datasets
|
|
151
|
-
- Implement proper chunking for oversized data
|
|
152
|
-
- Use compression for large text-based outputs
|
|
153
|
-
- Monitor upload progress and provide feedback
|
|
154
|
-
- Optimize serialization performance (use orjson over json)
|
|
155
|
-
|
|
156
|
-
**Object Store Optimization:**
|
|
157
|
-
|
|
158
|
-
- Use connection pooling for object store clients
|
|
159
|
-
- Implement proper retry logic for upload failures
|
|
160
|
-
- Use parallel uploads where appropriate
|
|
161
|
-
- Monitor upload metrics and error rates
|
|
162
|
-
- Handle bandwidth limitations gracefully
|
|
163
|
-
|
|
164
|
-
### Phase 5: Output Maintainability
|
|
165
|
-
|
|
166
|
-
**Error Handling and Recovery:**
|
|
167
|
-
|
|
168
|
-
- Implement comprehensive error handling for all upload operations
|
|
169
|
-
- Provide meaningful error messages with upload context
|
|
170
|
-
- Handle partial upload failures gracefully
|
|
171
|
-
- Implement proper retry logic for transient failures
|
|
172
|
-
- Log all upload operations with destination information
|
|
173
|
-
|
|
174
|
-
**Configuration Management:**
|
|
175
|
-
|
|
176
|
-
- Externalize all output-related configuration
|
|
177
|
-
- Support different output destinations and formats
|
|
178
|
-
- Validate output configuration before processing
|
|
179
|
-
- Document all supported output parameters
|
|
180
|
-
- Handle environment-specific output requirements
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Output-Specific Anti-Patterns
|
|
185
|
-
|
|
186
|
-
**Always Reject:**
|
|
187
|
-
|
|
188
|
-
- **Path derivation errors**: Deriving object store paths from local temporary paths
|
|
189
|
-
- **Sequential uploads**: Uploading multiple files sequentially when parallel uploads are possible
|
|
190
|
-
- **Memory inefficiency**: Loading entire datasets into memory for serialization
|
|
191
|
-
- **Missing upload verification**: Not verifying successful uploads
|
|
192
|
-
- **Poor error recovery**: Not handling partial upload failures gracefully
|
|
193
|
-
- **Resource leaks**: Not cleaning up temporary files or connections
|
|
194
|
-
|
|
195
|
-
**Object Store Upload Anti-Patterns:**
|
|
196
|
-
|
|
197
|
-
```python
|
|
198
|
-
# ❌ REJECT: Incorrect upload path handling
|
|
199
|
-
class BadOutputProcessor:
|
|
200
|
-
async def upload_results(self, results: List[dict]):
|
|
201
|
-
# Wrong: derives upload path from temporary local path
|
|
202
|
-
local_temp_file = "/tmp/results.json"
|
|
203
|
-
upload_key = get_object_store_prefix(local_temp_file) # Incorrect!
|
|
204
|
-
|
|
205
|
-
await self.object_store.upload_file(results, upload_key)
|
|
206
|
-
|
|
207
|
-
# ✅ REQUIRE: Correct upload path handling
|
|
208
|
-
class GoodOutputProcessor:
|
|
209
|
-
async def upload_results(
|
|
210
|
-
self,
|
|
211
|
-
results: List[dict],
|
|
212
|
-
output_prefix: str, # User-specified destination
|
|
213
|
-
filename: str = "results.json"
|
|
214
|
-
):
|
|
215
|
-
# Use actual user-configured output location
|
|
216
|
-
upload_key = os.path.join(output_prefix, filename)
|
|
217
|
-
|
|
218
|
-
await self.object_store.upload_file(
|
|
219
|
-
data=orjson.dumps(results),
|
|
220
|
-
destination=upload_key # Correct destination
|
|
221
|
-
)
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Performance Anti-Patterns:**
|
|
225
|
-
|
|
226
|
-
```python
|
|
227
|
-
# ❌ REJECT: Sequential upload processing
|
|
228
|
-
async def upload_multiple_files_sequential(file_data_pairs: List[Tuple]):
|
|
229
|
-
results = []
|
|
230
|
-
for data, filename in file_data_pairs: # Should be parallelized
|
|
231
|
-
result = await upload_single_file(data, filename)
|
|
232
|
-
results.append(result)
|
|
233
|
-
return results
|
|
234
|
-
|
|
235
|
-
# ✅ REQUIRE: Parallel upload processing with proper error handling
|
|
236
|
-
async def upload_multiple_files_parallel(
|
|
237
|
-
file_data_pairs: List[Tuple],
|
|
238
|
-
max_concurrency: int = 5
|
|
239
|
-
) -> List[dict]:
|
|
240
|
-
semaphore = asyncio.Semaphore(max_concurrency)
|
|
241
|
-
|
|
242
|
-
async def upload_with_semaphore(data, filename):
|
|
243
|
-
async with semaphore:
|
|
244
|
-
try:
|
|
245
|
-
return await upload_single_file(data, filename)
|
|
246
|
-
except Exception as e:
|
|
247
|
-
logger.error(f"Upload failed for {filename}: {e}")
|
|
248
|
-
return {"filename": filename, "status": "failed", "error": str(e)}
|
|
249
|
-
|
|
250
|
-
tasks = [upload_with_semaphore(data, filename) for data, filename in file_data_pairs]
|
|
251
|
-
return await asyncio.gather(*tasks)
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
**Memory Management Anti-Patterns:**
|
|
255
|
-
|
|
256
|
-
```python
|
|
257
|
-
# ❌ REJECT: Loading entire dataset for serialization
|
|
258
|
-
async def bad_large_dataset_upload(large_dataset: List[dict]):
|
|
259
|
-
# Loads entire dataset into memory
|
|
260
|
-
json_data = orjson.dumps(large_dataset) # Could exceed memory limits
|
|
261
|
-
await upload_data(json_data)
|
|
262
|
-
|
|
263
|
-
# ✅ REQUIRE: Streaming serialization for large datasets
|
|
264
|
-
async def good_large_dataset_upload(large_dataset: List[dict], chunk_size: int = 1000):
|
|
265
|
-
"""Stream large datasets to avoid memory issues."""
|
|
266
|
-
|
|
267
|
-
async def serialize_chunk(chunk: List[dict]) -> bytes:
|
|
268
|
-
return orjson.dumps(chunk, option=orjson.OPT_APPEND_NEWLINE)
|
|
269
|
-
|
|
270
|
-
# Process in chunks to manage memory
|
|
271
|
-
for i in range(0, len(large_dataset), chunk_size):
|
|
272
|
-
chunk = large_dataset[i:i + chunk_size]
|
|
273
|
-
serialized_chunk = await serialize_chunk(chunk)
|
|
274
|
-
|
|
275
|
-
await upload_chunk(
|
|
276
|
-
data=serialized_chunk,
|
|
277
|
-
chunk_index=i // chunk_size
|
|
278
|
-
)
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
## Educational Context for Output Reviews
|
|
282
|
-
|
|
283
|
-
When reviewing output code, emphasize:
|
|
284
|
-
|
|
285
|
-
1. **Data Integrity Impact**: "Incorrect upload path handling can cause data to be stored in wrong locations, making it inaccessible to downstream processes. This breaks data pipelines and can cause data loss."
|
|
286
|
-
|
|
287
|
-
2. **Performance Impact**: "Sequential uploads create unnecessary bottlenecks. For enterprise datasets with multiple output files, parallelization can significantly reduce processing time and improve user experience."
|
|
288
|
-
|
|
289
|
-
3. **Resource Impact**: "Poor memory management during serialization can cause out-of-memory errors with large datasets. Streaming and chunking are essential for enterprise-scale data output."
|
|
290
|
-
|
|
291
|
-
4. **User Experience Impact**: "Output path errors are often discovered late in processing, causing wasted computation and frustrating delays. Proper validation and clear error messages improve reliability."
|
|
292
|
-
|
|
293
|
-
5. **Scalability Impact**: "Output patterns that work for small datasets can fail at enterprise scale. Always design output processes to handle the largest expected dataset sizes efficiently."
|
|
294
|
-
|
|
295
|
-
6. **Data Pipeline Impact**: "Output processing is the final step in data pipelines. Failures here can invalidate all upstream processing work. Robust error handling and verification are critical for pipeline reliability."
|