atlan-application-sdk 1.1.1__py3-none-any.whl → 2.0.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.
Files changed (47) hide show
  1. application_sdk/activities/common/sql_utils.py +308 -0
  2. application_sdk/activities/common/utils.py +1 -45
  3. application_sdk/activities/metadata_extraction/sql.py +110 -353
  4. application_sdk/activities/query_extraction/sql.py +12 -11
  5. application_sdk/application/__init__.py +1 -1
  6. application_sdk/clients/sql.py +167 -1
  7. application_sdk/clients/temporal.py +6 -6
  8. application_sdk/common/types.py +8 -0
  9. application_sdk/common/utils.py +1 -8
  10. application_sdk/constants.py +1 -1
  11. application_sdk/handlers/sql.py +10 -25
  12. application_sdk/interceptors/events.py +1 -1
  13. application_sdk/io/__init__.py +654 -0
  14. application_sdk/io/json.py +429 -0
  15. application_sdk/{outputs → io}/parquet.py +358 -47
  16. application_sdk/io/utils.py +307 -0
  17. application_sdk/observability/observability.py +23 -12
  18. application_sdk/server/fastapi/middleware/logmiddleware.py +23 -17
  19. application_sdk/server/fastapi/middleware/metrics.py +27 -24
  20. application_sdk/server/fastapi/models.py +1 -1
  21. application_sdk/server/fastapi/routers/server.py +1 -1
  22. application_sdk/server/fastapi/utils.py +10 -0
  23. application_sdk/services/eventstore.py +4 -4
  24. application_sdk/services/secretstore.py +1 -1
  25. application_sdk/test_utils/hypothesis/strategies/outputs/json_output.py +0 -1
  26. application_sdk/test_utils/hypothesis/strategies/server/fastapi/__init__.py +1 -1
  27. application_sdk/version.py +1 -1
  28. application_sdk/worker.py +1 -1
  29. {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.0.0.dist-info}/METADATA +9 -11
  30. {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.0.0.dist-info}/RECORD +35 -42
  31. application_sdk/common/dataframe_utils.py +0 -42
  32. application_sdk/events/__init__.py +0 -5
  33. application_sdk/inputs/.cursor/BUGBOT.md +0 -250
  34. application_sdk/inputs/__init__.py +0 -168
  35. application_sdk/inputs/iceberg.py +0 -75
  36. application_sdk/inputs/json.py +0 -136
  37. application_sdk/inputs/parquet.py +0 -272
  38. application_sdk/inputs/sql_query.py +0 -271
  39. application_sdk/outputs/.cursor/BUGBOT.md +0 -295
  40. application_sdk/outputs/__init__.py +0 -453
  41. application_sdk/outputs/iceberg.py +0 -139
  42. application_sdk/outputs/json.py +0 -268
  43. /application_sdk/{events → interceptors}/models.py +0 -0
  44. /application_sdk/{common/dapr_utils.py → services/_utils.py} +0 -0
  45. {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.0.0.dist-info}/WHEEL +0 -0
  46. {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  47. {atlan_application_sdk-1.1.1.dist-info → atlan_application_sdk-2.0.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,453 +0,0 @@
1
- """Output module for handling data output operations.
2
-
3
- This module provides base classes and utilities for handling various types of data outputs
4
- in the application, including file outputs and object store interactions.
5
- """
6
-
7
- import gc
8
- import inspect
9
- import os
10
- from abc import ABC, abstractmethod
11
- from enum import Enum
12
- from typing import (
13
- TYPE_CHECKING,
14
- Any,
15
- AsyncGenerator,
16
- Dict,
17
- Generator,
18
- List,
19
- Optional,
20
- Union,
21
- cast,
22
- )
23
-
24
- import orjson
25
- from temporalio import activity
26
-
27
- from application_sdk.activities.common.models import ActivityStatistics
28
- from application_sdk.activities.common.utils import get_object_store_prefix
29
- from application_sdk.common.dataframe_utils import is_empty_dataframe
30
- from application_sdk.constants import ENABLE_ATLAN_UPLOAD, UPSTREAM_OBJECT_STORE_NAME
31
- from application_sdk.observability.logger_adaptor import get_logger
32
- from application_sdk.observability.metrics_adaptor import MetricType
33
- from application_sdk.services.objectstore import ObjectStore
34
-
35
- logger = get_logger(__name__)
36
- activity.logger = logger
37
-
38
-
39
- if TYPE_CHECKING:
40
- import daft # type: ignore
41
- import pandas as pd
42
-
43
-
44
- class WriteMode(Enum):
45
- """Enumeration of write modes for output operations."""
46
-
47
- APPEND = "append"
48
- OVERWRITE = "overwrite"
49
- OVERWRITE_PARTITIONS = "overwrite-partitions"
50
-
51
-
52
- class Output(ABC):
53
- """Abstract base class for output handlers.
54
-
55
- This class defines the interface for output handlers that can write data
56
- to various destinations in different formats.
57
-
58
- Attributes:
59
- output_path (str): Path where the output will be written.
60
- upload_file_prefix (str): Prefix for files when uploading to object store.
61
- total_record_count (int): Total number of records processed.
62
- chunk_count (int): Number of chunks the output was split into.
63
- """
64
-
65
- output_path: str
66
- output_prefix: str
67
- total_record_count: int
68
- chunk_count: int
69
- chunk_part: int
70
- buffer_size: int
71
- max_file_size_bytes: int
72
- current_buffer_size: int
73
- current_buffer_size_bytes: int
74
- partitions: List[int]
75
-
76
- def estimate_dataframe_record_size(self, dataframe: "pd.DataFrame") -> int:
77
- """Estimate File size of a DataFrame by sampling a few records."""
78
- if len(dataframe) == 0:
79
- return 0
80
-
81
- # Sample up to 10 records to estimate average size
82
- sample_size = min(10, len(dataframe))
83
- sample = dataframe.head(sample_size)
84
- file_type = type(self).__name__.lower().replace("output", "")
85
- compression_factor = 1
86
- if file_type == "json":
87
- sample_file = sample.to_json(orient="records", lines=True)
88
- else:
89
- sample_file = sample.to_parquet(index=False, compression="snappy")
90
- compression_factor = 0.01
91
- if sample_file is not None:
92
- avg_record_size = len(sample_file) / sample_size * compression_factor
93
- return int(avg_record_size)
94
-
95
- return 0
96
-
97
- def path_gen(
98
- self,
99
- chunk_count: Optional[int] = None,
100
- chunk_part: int = 0,
101
- start_marker: Optional[str] = None,
102
- end_marker: Optional[str] = None,
103
- ) -> str:
104
- """Generate a file path for a chunk.
105
-
106
- Args:
107
- chunk_start (Optional[int]): Starting index of the chunk, or None for single chunk.
108
- chunk_count (int): Total number of chunks.
109
- start_marker (Optional[str]): Start marker for query extraction.
110
- end_marker (Optional[str]): End marker for query extraction.
111
-
112
- Returns:
113
- str: Generated file path for the chunk.
114
- """
115
- # For Query Extraction - use start and end markers without chunk count
116
- if start_marker and end_marker:
117
- return f"{start_marker}_{end_marker}{self._EXTENSION}"
118
-
119
- # For regular chunking - include chunk count
120
- if chunk_count is None:
121
- return f"{str(chunk_part)}{self._EXTENSION}"
122
- else:
123
- return f"chunk-{str(chunk_count)}-part{str(chunk_part)}{self._EXTENSION}"
124
-
125
- def process_null_fields(
126
- self,
127
- obj: Any,
128
- preserve_fields: Optional[List[str]] = None,
129
- null_to_empty_dict_fields: Optional[List[str]] = None,
130
- ) -> Any:
131
- """
132
- By default the method removes null values from dictionaries and lists.
133
- Except for the fields specified in preserve_fields.
134
- And fields in null_to_empty_dict_fields are replaced with empty dict if null.
135
-
136
- Args:
137
- obj: The object to clean (dict, list, or other value)
138
- preserve_fields: Optional list of field names that should be preserved even if they contain null values
139
- null_to_empty_dict_fields: Optional list of field names that should be replaced with empty dict if null
140
-
141
- Returns:
142
- The cleaned object with null values removed
143
- """
144
- if isinstance(obj, dict):
145
- result = {}
146
- for k, v in obj.items():
147
- # Handle null fields that should be converted to empty dicts
148
- if k in (null_to_empty_dict_fields or []) and v is None:
149
- result[k] = {}
150
- continue
151
-
152
- # Process the value recursively
153
- processed_value = self.process_null_fields(
154
- v, preserve_fields, null_to_empty_dict_fields
155
- )
156
-
157
- # Keep the field if it's in preserve_fields or has a non-None processed value
158
- if k in (preserve_fields or []) or processed_value is not None:
159
- result[k] = processed_value
160
-
161
- return result
162
- return obj
163
-
164
- async def write_batched_dataframe(
165
- self,
166
- batched_dataframe: Union[
167
- AsyncGenerator["pd.DataFrame", None], Generator["pd.DataFrame", None, None]
168
- ],
169
- ):
170
- """Write a batched pandas DataFrame to Output.
171
-
172
- This method writes the DataFrame to Output provided, potentially splitting it
173
- into chunks based on chunk_size and buffer_size settings.
174
-
175
- Args:
176
- dataframe (pd.DataFrame): The DataFrame to write.
177
-
178
- Note:
179
- If the DataFrame is empty, the method returns without writing.
180
- """
181
- try:
182
- if inspect.isasyncgen(batched_dataframe):
183
- async for dataframe in batched_dataframe:
184
- if not is_empty_dataframe(dataframe):
185
- await self.write_dataframe(dataframe)
186
- else:
187
- # Cast to Generator since we've confirmed it's not an AsyncGenerator
188
- sync_generator = cast(
189
- Generator["pd.DataFrame", None, None], batched_dataframe
190
- )
191
- for dataframe in sync_generator:
192
- if not is_empty_dataframe(dataframe):
193
- await self.write_dataframe(dataframe)
194
- except Exception as e:
195
- logger.error(f"Error writing batched dataframe: {str(e)}")
196
- raise
197
-
198
- async def write_dataframe(self, dataframe: "pd.DataFrame"):
199
- """Write a pandas DataFrame to Parquet files and upload to object store.
200
-
201
- Args:
202
- dataframe (pd.DataFrame): The DataFrame to write.
203
- """
204
- try:
205
- if self.chunk_start is None:
206
- self.chunk_part = 0
207
- if len(dataframe) == 0:
208
- return
209
-
210
- chunk_size_bytes = self.estimate_dataframe_record_size(dataframe)
211
-
212
- for i in range(0, len(dataframe), self.buffer_size):
213
- chunk = dataframe[i : i + self.buffer_size]
214
-
215
- if (
216
- self.current_buffer_size_bytes + chunk_size_bytes
217
- > self.max_file_size_bytes
218
- ):
219
- output_file_name = f"{self.output_path}/{self.path_gen(self.chunk_count, self.chunk_part)}"
220
- if os.path.exists(output_file_name):
221
- await self._upload_file(output_file_name)
222
- self.chunk_part += 1
223
-
224
- self.current_buffer_size += len(chunk)
225
- self.current_buffer_size_bytes += chunk_size_bytes * len(chunk)
226
- await self._flush_buffer(chunk, self.chunk_part)
227
-
228
- del chunk
229
- gc.collect()
230
-
231
- if self.current_buffer_size_bytes > 0:
232
- # Finally upload the final file to the object store
233
- output_file_name = f"{self.output_path}/{self.path_gen(self.chunk_count, self.chunk_part)}"
234
- if os.path.exists(output_file_name):
235
- await self._upload_file(output_file_name)
236
- self.chunk_part += 1
237
-
238
- # Record metrics for successful write
239
- self.metrics.record_metric(
240
- name="write_records",
241
- value=len(dataframe),
242
- metric_type=MetricType.COUNTER,
243
- labels={"type": "pandas", "mode": WriteMode.APPEND.value},
244
- description="Number of records written to files from pandas DataFrame",
245
- )
246
-
247
- # Record chunk metrics
248
- self.metrics.record_metric(
249
- name="chunks_written",
250
- value=1,
251
- metric_type=MetricType.COUNTER,
252
- labels={"type": "pandas", "mode": WriteMode.APPEND.value},
253
- description="Number of chunks written to files",
254
- )
255
-
256
- # If chunk_start is set we don't want to increment the chunk_count
257
- # Since it should only increment the chunk_part in this case
258
- if self.chunk_start is None:
259
- self.chunk_count += 1
260
- self.partitions.append(self.chunk_part)
261
- except Exception as e:
262
- # Record metrics for failed write
263
- self.metrics.record_metric(
264
- name="write_errors",
265
- value=1,
266
- metric_type=MetricType.COUNTER,
267
- labels={
268
- "type": "pandas",
269
- "mode": WriteMode.APPEND.value,
270
- "error": str(e),
271
- },
272
- description="Number of errors while writing to files",
273
- )
274
- logger.error(f"Error writing pandas dataframe to files: {str(e)}")
275
- raise
276
-
277
- async def write_batched_daft_dataframe(
278
- self,
279
- batched_dataframe: Union[
280
- AsyncGenerator["daft.DataFrame", None], # noqa: F821
281
- Generator["daft.DataFrame", None, None], # noqa: F821
282
- ],
283
- ):
284
- """Write a batched daft DataFrame to JSON files.
285
-
286
- This method writes the DataFrame to JSON files, potentially splitting it
287
- into chunks based on chunk_size and buffer_size settings.
288
-
289
- Args:
290
- dataframe (daft.DataFrame): The DataFrame to write.
291
-
292
- Note:
293
- If the DataFrame is empty, the method returns without writing.
294
- """
295
- try:
296
- if inspect.isasyncgen(batched_dataframe):
297
- async for dataframe in batched_dataframe:
298
- if not is_empty_dataframe(dataframe):
299
- await self.write_daft_dataframe(dataframe)
300
- else:
301
- # Cast to Generator since we've confirmed it's not an AsyncGenerator
302
- sync_generator = cast(
303
- Generator["daft.DataFrame", None, None], batched_dataframe
304
- ) # noqa: F821
305
- for dataframe in sync_generator:
306
- if not is_empty_dataframe(dataframe):
307
- await self.write_daft_dataframe(dataframe)
308
- except Exception as e:
309
- logger.error(f"Error writing batched daft dataframe: {str(e)}")
310
-
311
- @abstractmethod
312
- async def write_daft_dataframe(self, dataframe: "daft.DataFrame"): # noqa: F821
313
- """Write a daft DataFrame to the output destination.
314
-
315
- Args:
316
- dataframe (daft.DataFrame): The DataFrame to write.
317
- """
318
- pass
319
-
320
- async def get_statistics(
321
- self, typename: Optional[str] = None
322
- ) -> ActivityStatistics:
323
- """Returns statistics about the output.
324
-
325
- This method returns a ActivityStatistics object with total record count and chunk count.
326
-
327
- Args:
328
- typename (str): Type name of the entity e.g database, schema, table.
329
-
330
- Raises:
331
- ValidationError: If the statistics data is invalid
332
- Exception: If there's an error writing the statistics
333
- """
334
- try:
335
- statistics = await self.write_statistics(typename)
336
- if not statistics:
337
- raise ValueError("No statistics data available")
338
- statistics = ActivityStatistics.model_validate(statistics)
339
- if typename:
340
- statistics.typename = typename
341
- return statistics
342
- except Exception as e:
343
- logger.error(f"Error getting statistics: {str(e)}")
344
- raise
345
-
346
- async def _upload_file(self, file_name: str):
347
- """Upload a file to the object store."""
348
- if ENABLE_ATLAN_UPLOAD:
349
- await ObjectStore.upload_file(
350
- source=file_name,
351
- store_name=UPSTREAM_OBJECT_STORE_NAME,
352
- retain_local_copy=True,
353
- destination=get_object_store_prefix(file_name),
354
- )
355
- await ObjectStore.upload_file(
356
- source=file_name,
357
- destination=get_object_store_prefix(file_name),
358
- )
359
-
360
- self.current_buffer_size_bytes = 0
361
-
362
- async def _flush_buffer(self, chunk: "pd.DataFrame", chunk_part: int):
363
- """Flush the current buffer to a JSON file.
364
-
365
- This method combines all DataFrames in the buffer, writes them to a JSON file,
366
- and uploads the file to the object store.
367
-
368
- Note:
369
- If the buffer is empty or has no records, the method returns without writing.
370
- """
371
- try:
372
- if not is_empty_dataframe(chunk):
373
- self.total_record_count += len(chunk)
374
- output_file_name = (
375
- f"{self.output_path}/{self.path_gen(self.chunk_count, chunk_part)}"
376
- )
377
- await self.write_chunk(chunk, output_file_name)
378
-
379
- self.current_buffer_size = 0
380
-
381
- # Record chunk metrics
382
- self.metrics.record_metric(
383
- name="chunks_written",
384
- value=1,
385
- metric_type=MetricType.COUNTER,
386
- labels={"type": "output"},
387
- description="Number of chunks written to files",
388
- )
389
-
390
- except Exception as e:
391
- # Record metrics for failed write
392
- self.metrics.record_metric(
393
- name="write_errors",
394
- value=1,
395
- metric_type=MetricType.COUNTER,
396
- labels={"type": "output", "error": str(e)},
397
- description="Number of errors while writing to files",
398
- )
399
- logger.error(f"Error flushing buffer to files: {str(e)}")
400
- raise e
401
-
402
- async def write_statistics(
403
- self, typename: Optional[str] = None
404
- ) -> Optional[Dict[str, Any]]:
405
- """Write statistics about the output to a JSON file.
406
-
407
- This method writes statistics including total record count and chunk count
408
- to a JSON file and uploads it to the object store.
409
-
410
- Raises:
411
- Exception: If there's an error writing or uploading the statistics.
412
- """
413
- try:
414
- # prepare the statistics
415
- statistics = {
416
- "total_record_count": self.total_record_count,
417
- "chunk_count": len(self.partitions),
418
- "partitions": self.partitions,
419
- }
420
-
421
- # Ensure typename is included in the statistics payload (if provided)
422
- if typename:
423
- statistics["typename"] = typename
424
-
425
- # Write the statistics to a json file inside a dedicated statistics/ folder
426
- statistics_dir = os.path.join(self.output_path, "statistics")
427
- os.makedirs(statistics_dir, exist_ok=True)
428
- output_file_name = os.path.join(statistics_dir, "statistics.json.ignore")
429
- # If chunk_start is provided, include it in the statistics filename
430
- try:
431
- cs = getattr(self, "chunk_start", None)
432
- if cs is not None:
433
- output_file_name = os.path.join(
434
- statistics_dir, f"statistics-chunk-{cs}.json.ignore"
435
- )
436
- except Exception:
437
- # If accessing chunk_start fails, fallback to default filename
438
- pass
439
-
440
- # Write the statistics dictionary to the JSON file
441
- with open(output_file_name, "wb") as f:
442
- f.write(orjson.dumps(statistics))
443
-
444
- destination_file_path = get_object_store_prefix(output_file_name)
445
- # Push the file to the object store
446
- await ObjectStore.upload_file(
447
- source=output_file_name,
448
- destination=destination_file_path,
449
- )
450
-
451
- return statistics
452
- except Exception as e:
453
- logger.error(f"Error writing statistics: {str(e)}")
@@ -1,139 +0,0 @@
1
- from typing import TYPE_CHECKING, Union
2
-
3
- from pyiceberg.catalog import Catalog
4
- from pyiceberg.table import Table
5
- from temporalio import activity
6
-
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.outputs import Output
10
-
11
- logger = get_logger(__name__)
12
- activity.logger = logger
13
-
14
- if TYPE_CHECKING:
15
- import daft
16
- import pandas as pd
17
-
18
-
19
- class IcebergOutput(Output):
20
- """
21
- Iceberg Output class to write data to Iceberg tables using daft and pandas
22
- """
23
-
24
- def __init__(
25
- self,
26
- iceberg_catalog: Catalog,
27
- iceberg_namespace: str,
28
- iceberg_table: Union[str, Table],
29
- mode: str = "append",
30
- total_record_count: int = 0,
31
- chunk_count: int = 0,
32
- retain_local_copy: bool = False,
33
- ):
34
- """Initialize the Iceberg output class.
35
-
36
- Args:
37
- iceberg_catalog (Catalog): Iceberg catalog object.
38
- iceberg_namespace (str): Iceberg namespace.
39
- iceberg_table (Union[str, Table]): Iceberg table object or table name.
40
- mode (str, optional): Write mode for the iceberg table. Defaults to "append".
41
- total_record_count (int, optional): Total record count written to the iceberg table. Defaults to 0.
42
- chunk_count (int, optional): Number of chunks written to the iceberg table. Defaults to 0.
43
- retain_local_copy (bool, optional): Whether to retain the local copy of the files.
44
- Defaults to False.
45
- """
46
- self.total_record_count = total_record_count
47
- self.chunk_count = chunk_count
48
- self.iceberg_catalog = iceberg_catalog
49
- self.iceberg_namespace = iceberg_namespace
50
- self.iceberg_table = iceberg_table
51
- self.mode = mode
52
- self.metrics = get_metrics()
53
- self.retain_local_copy = retain_local_copy
54
-
55
- async def write_dataframe(self, dataframe: "pd.DataFrame"):
56
- """
57
- Method to write the pandas dataframe to an iceberg table
58
- """
59
- try:
60
- import daft
61
-
62
- if len(dataframe) == 0:
63
- return
64
- # convert the pandas dataframe to a daft dataframe
65
- daft_dataframe = daft.from_pandas(dataframe)
66
- await self.write_daft_dataframe(daft_dataframe)
67
-
68
- # Record metrics for successful write
69
- self.metrics.record_metric(
70
- name="iceberg_write_records",
71
- value=len(dataframe),
72
- metric_type=MetricType.COUNTER,
73
- labels={"mode": self.mode, "type": "pandas"},
74
- description="Number of records written to Iceberg table from pandas DataFrame",
75
- )
76
- except Exception as e:
77
- # Record metrics for failed write
78
- self.metrics.record_metric(
79
- name="iceberg_write_errors",
80
- value=1,
81
- metric_type=MetricType.COUNTER,
82
- labels={"mode": self.mode, "type": "pandas", "error": str(e)},
83
- description="Number of errors while writing to Iceberg table",
84
- )
85
- logger.error(f"Error writing pandas dataframe to iceberg table: {str(e)}")
86
- raise e
87
-
88
- async def write_daft_dataframe(self, dataframe: "daft.DataFrame"): # noqa: F821
89
- """
90
- Method to write the daft dataframe to an iceberg table
91
- """
92
- try:
93
- if dataframe.count_rows() == 0:
94
- return
95
- # Create a new table in the iceberg catalog
96
- self.chunk_count += 1
97
- self.total_record_count += dataframe.count_rows()
98
-
99
- # check if iceberg table is already created
100
- if isinstance(self.iceberg_table, Table):
101
- # if yes, use the existing iceberg table
102
- table = self.iceberg_table
103
- else:
104
- # if not, create a new table in the iceberg catalog
105
- table = self.iceberg_catalog.create_table_if_not_exists(
106
- f"{self.iceberg_namespace}.{self.iceberg_table}",
107
- schema=dataframe.to_arrow().schema,
108
- )
109
- # write the dataframe to the iceberg table
110
- dataframe.write_iceberg(table, mode=self.mode)
111
-
112
- # Record metrics for successful write
113
- self.metrics.record_metric(
114
- name="iceberg_write_records",
115
- value=dataframe.count_rows(),
116
- metric_type=MetricType.COUNTER,
117
- labels={"mode": self.mode, "type": "daft"},
118
- description="Number of records written to Iceberg table from daft DataFrame",
119
- )
120
-
121
- # Record chunk metrics
122
- self.metrics.record_metric(
123
- name="iceberg_chunks_written",
124
- value=1,
125
- metric_type=MetricType.COUNTER,
126
- labels={"mode": self.mode},
127
- description="Number of chunks written to Iceberg table",
128
- )
129
- except Exception as e:
130
- # Record metrics for failed write
131
- self.metrics.record_metric(
132
- name="iceberg_write_errors",
133
- value=1,
134
- metric_type=MetricType.COUNTER,
135
- labels={"mode": self.mode, "type": "daft", "error": str(e)},
136
- description="Number of errors while writing to Iceberg table",
137
- )
138
- logger.error(f"Error writing daft dataframe to iceberg table: {str(e)}")
139
- raise e