[Python] Calling snowflake udtf from snowflake stored procedure

Discussão em 'Python' iniciado por Stack, Julho 12, 2025.

  1. Stack

    Stack Membro Participativo

    I am trying to write a udtf in snowflake which I need to call from stored procedure. I would want to register the udtf as part of session using snowpark. Also, want to call it with partition by so it runs for each partition instead of running for each record because I want to run the tranformation serially inside each partition to control the level of parallelism.

    below is the code I pulled up so far, but it complains that it is not able to find process method.

    Procedure error: (1304): 01bd9ed6-020e-0336-003b-dd0712547cce: 100357 (P0000): 01bd9ed6-020e-0336-003b-dd0712547cce: Could not find process method in function FILE_PROCESSOR_UDTF with handler compute"

    below is my definition of stored procedure (terraform):

    resource "snowflake_procedure_python" "file_processor" {
    name = "FILE_PROCESSOR"
    comment = "Process files using UDTF with file URL input"
    database = var.database_name
    schema = var.application_schema_name
    snowpark_package = var.snowpark_version
    runtime_version = var.python_version
    handler = "transform_snowflake.procedures.file_processor.main"
    # return_type = "TABLE(FILE_URL VARCHAR, FILE_SIZE NUMBER, STATUS VARCHAR, CONTENT_PREVIEW VARCHAR, ERROR_MESSAGE VARCHAR)"
    return_type = "VARIANT"
    execute_as = "CALLER"

    arguments {
    arg_name = "STAGE_PATH"
    arg_data_type = "VARCHAR"
    }
    arguments {
    arg_name = "FILE_PATTERN"
    arg_data_type = "VARCHAR"
    arg_default_value = "'*'"
    }
    arguments {
    arg_name = "MAX_FILES"
    arg_data_type = "NUMBER"
    arg_default_value = 10
    }

    imports {
    stage_location = var.code_stage_fully_qualified_name
    path_on_stage = "transform_snowflake.zip"
    }
    }


    below is the handler code for this stored procedure which is calling table function:

    import snowflake.snowpark as snowpark
    from snowflake.snowpark import Session
    import pandas as pd
    import logging
    import traceback
    from snowflake.snowpark.types import StringType, StructField, StructType, VariantType, IntegerType
    from transform_snowflake.udtfs.file_processor_udtf import FileProcessorUDTF

    logger = logging.getLogger("transform_snowflake.procedures.file_processor")

    def main(session: Session, stage_path: str, file_pattern: str = '*', max_files: int = 10):
    """
    Main stored procedure function that registers UDTF and processes files
    """
    try:
    logger.info(f"Starting file processor for stage: {stage_path}, pattern: {file_pattern}")

    output_schema = StructType([
    StructField("FILE_URL", StringType()),
    StructField("FILE_SIZE", IntegerType()),
    StructField("STATUS", StringType()),
    StructField("CONTENT_PREVIEW", StringType()),
    StructField("ERROR_MESSAGE", StringType())
    ])
    file_processor_udtf = session.udtf.register(
    handler=FileProcessorUDTF,
    name="file_processor_udtf",
    input_types=[StringType()],
    output_schema=output_schema,
    replace=True,
    is_permanent=False,
    packages=['snowflake-snowpark-python', 'pgpy', 'pandas']
    )

    # Adding two rows in dataframe for testing purposes
    df_files = session.create_dataframe([{
    'FILE_URL': '<stage_name>/encrypted_test/sample_plaintext.txt.pgp'
    },
    {
    'FILE_URL': '<stage_name>/encrypted_test/sample_plaintext2.txt.pgp'
    }])
    # Check if we have any files
    file_count = df_files.count()
    if file_count == 0:
    logger.warning(f"No files found in {stage_path} with pattern {file_pattern}")
    # Return empty result with correct schema
    return session.create_dataframe([{
    'FILE_URL': 'No files found',
    'FILE_SIZE': 0,
    'STATUS': 'NO_FILES',
    'CONTENT_PREVIEW': '',
    'ERROR_MESSAGE': f'No files found in {stage_path} with pattern {file_pattern}'
    }])

    logger.info(f"Found {file_count} files to process")

    # Use the UDTF to process files
    df_columns = df_files.columns
    result_df = df_files.join_table_function(
    file_processor_udtf(*df_columns).over(partition_by='FILE_URL'))

    # Collect and return results
    return result_df.collect()

    except Exception as e:
    logger.error(f"Error in file processor: {str(e)}")
    logger.error(traceback.format_exc())

    # Return error result
    return session.create_dataframe([{
    'FILE_URL': stage_path,
    'FILE_SIZE': 0,
    'STATUS': 'PROCEDURE_ERROR',
    'CONTENT_PREVIEW': '',
    'ERROR_MESSAGE': f'Procedure error: {str(e)}'
    }]).collect()


    And lastly, the table function:

    import pandas as pd
    import time
    import logging
    from snowflake.snowpark.files import SnowflakeFile
    from _snowflake import vectorized
    import threading

    logger = logging.getLogger("udtfs.file_processor_udtf")

    class FileProcessorUDTF:
    """
    UDTF class to process files from stage URLs using vectorized processing
    """

    def __init__(self):
    self.lock = threading.Lock()

    @vectorized(input=pd.DataFrame)
    def end_partition(self, df: pd.DataFrame):
    """
    Vectorized end_partition function that processes files sequentially
    """
    raise Exception('inside end_partition')
    logger.info(f"Processing partition with {len(df)} files")

    results = []

    # Process files sequentially with simple for loop
    for idx, row in df.iterrows():
    try:
    result = self._process_single_file(row['file_url'])
    results.append(result)
    except Exception as e:
    # Handle unexpected errors in file processing
    error_result = {
    'file_url': row['file_url'],
    'file_size': 0,
    'status': 'FAILED',
    'content_preview': '',
    'error_message': f'Processing error: {str(e)}'
    }
    results.append(error_result)

    # Convert results to DataFrame
    result_df = pd.DataFrame(results)

    logger.info(f"Successfully processed {len(results)} files in partition")
    return result_df

    def _process_single_file(self, file_url):
    """
    Process a single file using SnowflakeFile
    """
    start_time = time.time()

    result = {
    'FILE_URL': file_url,
    'FILE_SIZE': 0,
    'STATUS': 'FAILED',
    'CONTENT_PREVIEW': '',
    'ERROR_MESSAGE': ''
    }

    try:
    # Open file using SnowflakeFile
    with SnowflakeFile.open(file_url, 'rb') as f:
    # Get file size
    if f.seekable():
    current_pos = f.tell()
    f.seek(0, 2) # Seek to end
    file_size = f.tell()
    f.seek(current_pos) # Return to original position

    # Read first 500 bytes for preview
    preview_data = f.read(500)
    f.seek(current_pos) # Reset position
    else:
    # If not seekable, read in chunks
    preview_data = f.read(500)
    file_size = len(preview_data)

    # Convert preview to string (handle binary data)
    try:
    content_preview = preview_data.decode('utf-8', errors='replace')[:200]
    except:
    content_preview = f"Binary data ({len(preview_data)} bytes)"

    # Update result with success
    result.update({
    'FILE_SIZE': file_size,
    'STATUS': 'SUCCESS',
    'CONTENT_PREVIEW': content_preview,
    'ERROR_MESSAGE': ''
    })

    except Exception as e:
    # Handle file processing errors
    result.update({
    'STATUS': 'FAILED',
    'ERROR_MESSAGE': str(e)[:500] # Truncate long error messages
    })

    return result


    Below is some information from snowflake documentation that mentions about having either end_partition or process. so not sure what is going wrong

    https://docs.snowflake.com/en/developer-guide/udf/python/udf-python-tabular-vectorized

    Continue reading...

Compartilhe esta Página