Carregamento e execução de funções personalizadas em uma sala limpa

Visão geral

Você pode carregar UDFs e UDTFs Python personalizadas em sua sala limpa e executá-las a partir de seus modelos para realizar ações de dados complexas. Essas ações incluem machine learning ou manipulação de dados personalizados em uma consulta, como parte de uma etapa única ou de um fluxo de várias etapas. Python é a única linguagem de codificação compatível com UDFs personalizadas.

O código carregado pode importar e usar pacotes de um conjunto aprovado de pacotes Python e da Snowpark API.

Tanto provedores quanto consumidores podem carregar um código Python personalizado em uma sala limpa, embora o processo seja diferente para cada.

Este tópico mostra como carregar e executar UDFs e UDTFs do Python personalizados como um provedor ou um consumidor.

Dica

Para obter informações básicas de como desenvolver as próprias UDFs Python em uma sala limpa, consulte os seguintes tópicos:

Execução do código carregado

Cada pacote do código carregado pode definir várias funções que chamam umas às outras, mas um pacote expõe apenas uma função do manipulador. Essa função do manipulador pode ser chamada por modelos criados ou executados por qualquer pessoa que use a sala limpa. Se o código criar tabelas internas, essas tabelas poderão ser acessadas conforme descrito em Projeto de fluxos de várias etapas.

Por exemplo, se você carregou uma função chamada simple_add que aceita dois parâmetros numéricos, pode chamá-la a partir de um modelo, conforme mostrado aqui. A função é sempre referenciada usando o escopo cleanroom. Por exemplo, um modelo poderia chamar simple_add assim:

SELECT cleanroom.simple_add({{ price | sqlsafe | int }}, {{ tax | sqlsafe | int }}) ...
Copy

Dica

Se o provedor quiser executar o código acima, ele deverá criar um alias para todas as colunas SELECT que usam uma função agregada ou personalizada, porque uma tabela de resultados é gerada em segundo plano:

SELECT
  cleanroom.simple_add(
    {{ price | sqlsafe | int }}, {{ tax | sqlsafe | int }}
    ) AS TOTAL_ITEM_COST
...
Copy

Você pode carregar várias funções em um único pacote, e funções dentro de um único pacote podem chamar umas às outras, mas funções não podem chamar funções dentro de outros pacotes (entretanto, elas podem chamar as funções do manipulador). Por exemplo:

Sala limpa com dois pacotes Python enviados

Pacote 1

Pacote 2

  • Função do manipulador A

  • Função auxiliar A1

  • Função auxiliar A2

  • Função do manipulador B

  • Função auxiliar B1

  • Função auxiliar B2

Notas:

  • O código enviado por qualquer um dos lados (provedor e consumidor) pode ser executado por qualquer um dos lados.

  • Um modelo pode chamar a função A ou a função B, mas não A1, A2, B1, B2.

  • A função A pode chamar a função B e vice-versa.

  • A função A não pode chamar B1 ou B2 e a função B não pode chamar A1 ou A2.

  • A1 pode chamar A2 e vice-versa. A1 e A2 podem chamar B. A1 e A2 não podem chamar B1 ou B2.

  • B1 pode chamar B2 e vice-versa. B1 e B2 podem chamar A. B1 e B2 não podem chamar A1 ou A2.

Atualização ou exclusão de funções personalizadas

Você pode carregar ou substituir uma função ou modelo existente, mas não pode excluí-los. A única maneira de «remover» uma função é criar uma função fictícia com exatamente o mesmo nome e assinatura que sempre funciona.

O carregamento de uma função com a assinatura exata da função já carregada substitui a função existente. A assinatura é o nome da função que não diferencia maiúsculas e minúsculas do manipulador externo, e os tipos de dados de todos os parâmetros, na mesma ordem. Os nomes dos parâmetros não têm importância. Você não pode substituir uma função carregada por outra conta.

Como a assinatura deve ser igual quando você atualiza uma função, não é possível alterar a assinatura de uma função existente: se você carregar a função foo(name VARIANT age INTEGER) e, em seguida, carregar a função foo(name VARIANT age FLOAT), a segunda função será adicionada à sala limpa junto com a primeira, pois os tipos de argumento são diferentes.

Código enviado pelo provedor

As funções enviadas pelo provedor podem ser carregadas como código em linha ou de uma área de preparação do Snowflake. Ambas as técnicas são abordadas aqui.

O código carregado pode importar e usar pacotes nativamente de um conjunto aprovado de pacotes Python. Se você precisar de um pacote não padrão, use o Snowpark Container Services em uma sala limpa para hospedar o código.

Você não pode visualizar o código do provedor carregado, nem mesmo o seu próprio código; portanto, inclua uma cópia exata do que você carregou em uma sala limpa.

Dica

Depois de atualizar o código escrito pelo provedor, você deverá atualizar a diretiva de lançamento padrão e chamar provider.create_or_update_cleanroom_listing para propagar as alterações aos consumidores. Se você não chamar provider.create_or_update_cleanroom_listing, sua versão padrão não será atualizada para os consumidores que estiverem usando a sala limpa.

Confira abaixo uma exibição de alto nível de como um provedor adiciona código a uma sala limpa:

  1. O provedor cria e configura a sala limpa como de costume.

  2. O provedor carrega o código chamando provider.load_python_into_cleanroom. Você pode carregar seu código em linha diretamente desse procedimento ou carregar um arquivo de código para uma área de preparação e, depois disso, fornecer a localização da área de preparação para esse procedimento.

    O código pode incluir várias funções, mas apenas um manipulador é exposto para cada carregamento. Para expor várias funções aos modelos, carregue cada manipulador chamando provider.load_python_into_cleanroom.

  3. Após cada carregamento de código bem-sucedido, uma nova versão de patch da sala limpa é gerada. Você deve aumentar a versão padrão chamando provider.set_default_release_directive com o novo número de patch. Se a sala limpa for exposta externamente, serão executadas verificações de segurança antes de instalar o código, e você deverá chamar provider.view_cleanroom_scan_status para confirmar que as verificações de segurança foram aprovadas antes de incrementar a versão padrão.

    • Se você quiser carregar várias funções em um único patch, poderá carregar seu código em massa. No entanto, uploads em massa poderão dificultar a depuração se o upload apresentar um problema de verificação de segurança, pois o arquivo que causou o problema não é relatado na resposta de erro.

  4. Você cria e carrega um modelo personalizado que chama o código. O modelo deve chamar a função do manipulador usando o escopo cleanroom, ou seja: cleanroom.my_function(...).

  5. O consumidor executa seu modelo da mesma forma que qualquer outro modelo.

    Dica

    Se o consumidor encontrar um erro de montagem ao instalar uma sala limpa com o código personalizado, talvez seja indicação de erro de sintaxe no código.

Você encontra exemplos de código que demonstram esse fluxo na seção de exemplo de código escrito pelo provedor.

Observações importantes sobre controle de versão

Toda vez que o provedor carrega uma função, ele aumenta o número do patch (e há um limite de 99 números). Portanto, faça o possível para testar e depurar seu código antes de adicioná-lo à sala limpa para reduzir as atualizações de versão durante o desenvolvimento.

Se você atualizar um número de patch, os clientes que usam a UI de salas limpas podem ter que atualizar a página para ver a alteração. Os clientes que usam a API devem ver as mudanças imediatamente, mas pode haver um atraso, dependendo dos recursos disponíveis. Saiba mais sobre o controle de versão de sala limpa.

Carregamento de funções em linha escritas pelo provedor

Você pode carregar o código em linha no parâmetro code de provider.load_python_into_cleanroom. Veja um exemplo de carregamento de uma função simples em linha:

CALL samooha_by_snowflake_local_db.provider.load_python_into_cleanroom(
$cleanroom_name,
'simple_add',                         -- Name used to call the UDF from a template.
['first INTEGER', 'second INTEGER'],  -- Arguments of the UDF, specified as '<variable_name> <SQL type>' pairs.
['numpy', 'pandas'],                  -- Packages imported by the UDF.
'INTEGER',                            -- SQL return type of UDF.
'add_two',                            -- Handler function in your code called when external name is called.
$$
import numpy as np   # Not used, but you can load supported packages.
import pandas as pd

def add_two(first, second):
    return first + second
$$
);
Copy

O modelo que faz a chamada chama cleanroom.simple_add para invocar essa função. Os exemplos do provedor demonstram como carregar um código em linha.

Carregamento de funções escritas pelo provedor de uma área de preparação

Você pode carregar arquivos Python em uma área de preparação da sala limpa e mencionar essa área quando chamar provider.load_python_into_cleanroom. Carregar o código de uma área de preparação permite desenvolvê-lo em um editor no sistema local, evitar erros ao copiar/colar durante seu carregamento em linha e também melhorar o controle de versão. Observe que você pode carregar vários arquivos em uma única chamada de procedimento, mas apenas uma função do manipulador é exposta para cada carregamento.

O código é carregado de uma área de preparação para a sala limpa quando você chama load_python_into_cleanroom. As alterações posteriores feitas no código na área de preparação não serão propagadas à sala limpa.

Para carregar a UDF para uma área de preparação:

  1. Crie seu arquivo .py e disponibilize-o em uma localização onde você possa carregá-lo em uma área de preparação do Snowsight.

  2. Para saber o nome da área de preparação da sua sala limpa, chame provider.get_stage_for_python_files($cleanroom_name). Essa área é acessível pela sala limpa. Não é possível usar uma área de preparação arbitrária que você cria.

  3. Carregue o arquivo .py para a área de preparação da sua sala limpa. Há várias maneiras de fazer isso, inclusive usando a CLI, o Snowsight ou drivers específicos da linguagem.

  4. Chame provider.load_python_into_cleanroom com a localização da área de preparação, o manipulador, o nome externo, os argumentos e o tipo de retorno. Os modelos em sua sala limpa agora podem chamar a função.

O código de exemplo a seguir mostra como carregar o código em uma sala limpa de uma área de preparação.

-- Save the following code as reverser.py:
--import numpy as np
--def main(some_string):
--  '''Return the reverse of a string plus a random number 1-10'''
--  return some_string[::-1] + str(np.random.randint(1,10))

-- Get the stage for your clean room.
CALL samooha_by_snowflake_local_db.provider.get_stage_for_python_files($cleanroom_name);

-- Save the file to the stage. Here is how to do it by using the Snowflake CLI
PUT file://~/reverser.py <STAGE_NAME> overwrite=True auto_compress=False;

-- Load the code from the stage into the clean room.
CALL samooha_by_snowflake_local_db.provider.load_python_into_cleanroom(
    $cleanroom_name,
    'reverse', -- Name used to call the function
    ['some_string  STRING'], -- Arguments and SQL types
    ['numpy'],               -- Any required packages
    ['/reverser.py'],        -- Relative path to file on stage
    'STRING',                -- Return type
    'reverser.main'          -- <FILE_NAME>.<FUNCTION_NAME>
);

-- Uploading code, even from a stage, increases the patch number.
CALL samooha_by_snowflake_local_db.provider.set_default_release_directive(
  $cleanroom_name, 'V1_0', <NEW_PATCH_NUMBER>);

-- Upload a template that calls the function.
CALL samooha_by_snowflake_local_db.provider.add_custom_sql_template(
    $cleanroom_name,
    $udf_template_name,
    $$
    SELECT
      p.status,
      cleanroom.reverse(p.status)
    FROM SAMOOHA_SAMPLE_DATABASE.DEMO.CUSTOMERS AS p
    LIMIT 100;
    $$
);

-- Switch to the consumer account and run the template to see the results.
Copy

Os exemplos do provedor demonstram o carregamento do código de uma área de preparação.

Solução de erros de sintaxe ou falhas de verificação no código carregado

Se você carregar uma função que falhe devido a um erro de sintaxe ou se uma verificação de segurança falhar, um patch não publicável poderá ser gerado. Portanto, você deve testar seu código minuciosamente antes do upload para garantir que ele não contenha erros de sintaxe.

Você pode ver a lista de pacotes e os status de revisão deles executando o seguinte comando SQL e fornecendo o ID da sala limpa no local indicado:

SHOW VERSIONS IN APPLICATION PACKAGE samooha_cleanroom_cleanroom_id;

Verificações de segurança

Uma verificação de segurança é executada após qualquer ação que gere uma nova versão de patch em uma sala limpa externa, como quando o provedor carrega o Python na sala limpa (o código enviado pelo consumidor, descrito nesta página, não aciona uma verificação de segurança). As salas limpas internas não executam verificações de segurança, mas se você alterar uma sala limpa interna para uma sala limpa externa, isso acionará uma verificação de segurança para esse patch. Um patch de sala limpa não pode ser publicado externamente até que o patch seja verificado.

O Snowflake Data Clean Rooms usa a estrutura de verificação de segurança do aplicativo Snowflake nativo. Siga as práticas recomendadas de segurança para o aplicativo nativo para evitar erros de verificação de segurança.

Você pode executar ações adicionais de criação de patches antes da conclusão da última verificação de segurança. No entanto, você deve aguardar que provider.view_cleanroom_scan_status mostre sucesso antes de poder atualizar a diretiva de lançamento padrão para servir a versão mais recente da sala limpa.

Carregamento de várias funções Python em um único patch

Se você quiser carregar vários pacotes Python chamáveis ​​por modelo para sua sala limpa, chame prepare_python_for_cleanroom várias vezes e, em seguida, load_prepared_python_into_cleanroom uma vez para verificar, carregar e gerar um único patch para sua sala limpa. O exemplo a seguir demonstra o carregamento de um UDF e um UDTF usando o carregamento em massa:

---- Add custom inline UDF ----
CALL samooha_by_snowflake_local_db.provider.prepare_python_for_cleanroom(
    $cleanroom_name,
    'get_next_status',  -- Name of the UDF. Can be different from the handler.
    ['status VARCHAR'], -- Arguments of the UDF, specified as (variable name, SQL type).
    ['numpy'],          -- Packages needed by UDF.
    [],                 -- When providing the code inline, this is an empty array.
    'VARCHAR',          -- Return type of UDF.
    'get_next_status',  -- Handler.
    $$
import numpy as np
def get_next_status(status):
  """Return the next higher status, or a random status
  if no matching status found or at the top of the list."""

  statuses = ['MEMBER', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']
  try:
    return statuses[statuses.index(status.upper()) + 1]
  except:
    return 'NO MATCH'
    $$
);

---- Add custom inline UDTF. ----
CALL samooha_by_snowflake_local_db.provider.prepare_python_for_cleanroom(
    $cleanroom_name,
    'get_info',  -- Name of the UDTF. Can be different from the handler.
    ['hashed_email VARCHAR', 'days_active INT', 'status VARCHAR', 'income VARCHAR'],   -- Name/Type arguments of the UDTF.
    ['numpy'],         -- Packages used by UDTF.
    [],                -- When providing the code inline, this is an empty array.
    'TABLE(hashed_email VARCHAR, months_active INT, level VARCHAR)',  -- Return type of UDTF.
    'GetSomeVals',     -- Handler class name.
$$
class GetSomeVals:
  def __init__(self):
    self.month_days = 30

  def process(self, hashed_email, days_active, status, income):
    '''Change days into rough months, and also return whether we
    think the user's membership status is lower, higher, or equal to
    what is expected, based on their income.'''

    months_active = days_active // self.month_days
    brackets = ['0-50K', '50K-100K', '100K-250K', '250K+']
    statuses = ['MEMBER', 'SILVER', 'GOLD', 'PLATINUM']
    if(statuses.index(status) < brackets.index(income)):
      level = 'low'
    elif(statuses.index(status) > brackets.index(income)):
      level = 'high'
    else:
      level = 'equal'

    yield(hashed_email, months_active, level)
$$
);

-- Upload all stored procedures.
-- Note the new patch number returned by this procedure. Keep this number for later use.
CALL samooha_by_snowflake_local_db.provider.load_prepared_python_into_cleanroom($cleanroom_name);

-- Set the release directive specified by the last load_python_into_cleanroom call.
CALL samooha_by_snowflake_local_db.provider.set_default_release_directive($cleanroom_name, 'V1_0', <PATCH_NUMBER>);
Copy

Exemplos de código escrito pelo provedor

Os exemplos a seguir demonstram como adicionar UDFs e UDTFs escritas pelo provedor a uma sala limpa.

Baixe os exemplos a seguir e carregue-os como arquivos de planilha em sua conta Snowflake. Você precisa de contas separadas para o provedor e o consumidor, cada uma com a API de salas limpas instalada. Substitua as informações conforme indicado nos arquivos de amostra.

Código enviado pelo consumidor

O código carregado pelo consumidor é agrupado e carregado com um modelo personalizado usando o fluxo de upload de modelo do consumidor. O código carregado pode ser chamado por qualquer modelo na sala limpa.

Para fazer upload de código como consumidor, você deve compreender a sintaxe de modelo personalizado.

Observe que qualquer código carregado por um consumidor pode ser visto pelo provedor quando ele solicita permissão de carregamento. O código do consumidor também fica visível sempre que um provedor ou consumidor examina o modelo.

Confira abaixo uma visão geral das etapas para carregar o código do consumidor personalizado:

  1. O provedor cria a sala limpa como de costume e convida o consumidor.

  2. O consumidor instala e configura a sala limpa como de costume.

  3. O consumidor prepara um modelo que chama UDF ou UDTF dentro do namespace cleanroom. Por exemplo, para chamar a função calculate_tax definida pelo consumidor, um modelo simples pode parecer com o seguinte trecho:

    SELECT {{ cleanroom.calculate_tax(p.cost) }} AS Tax FROM my_db.my_sch.sales AS p;
    
    Copy
  4. O consumidor prepara o código Python. Recomendamos o uso de aspas duplas (" ") em vez de aspas simples (' ') no código para evitar ter que inserir um caractere de escape extra no futuro. Seu código pode fazer referência a estas bibliotecas Python aceitas.

  5. O consumidor passa o código Python para consumer.generate_python_request_template. O procedimento retorna o código Python como um procedimento armazenado, com um espaço reservado para o modelo JinjaSQL personalizado. Há várias cadeias de caracteres multilinha no modelo que usam $$ como delimitadores de várias linhas.

  6. O consumidor substitui o espaço reservado para o modelo na saída de generate_python_request_template pelo modelo JinjaSQL.

  7. No modelo combinado, insira um caractere de escape para aspas simples como esta: \'. Isso ocorre porque aspas simples serão usadas como o delimitador mais externo para toda a cadeia de caracteres do procedimento multilinha quando você enviá-la para a sala limpa. Aqui está um exemplo de um procedimento armazenado que inclui o código Python do consumidor e o modelo personalizado, com escape de caracteres:

      BEGIN
    
      CREATE OR REPLACE FUNCTION CLEANROOM.custom_compare(min_status STRING, max_status STRING, this_status STRING)
      RETURNS boolean
      LANGUAGE PYTHON
      RUNTIME_VERSION = 3.10
      PACKAGES = (\'numpy\')
    
      HANDLER = \'custom_compare\'
      AS $$
      import numpy as np
    
      def custom_compare(min_status:str, max_status:str, this_status:str):
        statuses = [\'MEMBER\', \'SILVER\', \'GOLD\', \'PLATINUM\']
        return ((statuses.index(this_status) >= statuses.index(min_status)) &
                (statuses.index(this_status) <= statuses.index(max_status)))
      $$;
    
      -- Custom template
      LET SQL_TEXT varchar := $$
      SELECT
        c.status,
        c.hashed_email
      FROM IDENTIFIER( {{ my_table[0] }} ) as c
      WHERE cleanroom.custom_compare({{ min_status }}, {{ max_status }}, c.status);
      $$;
    
      LET RES resultset := (EXECUTE IMMEDIATE :SQL_TEXT);
      RETURN TABLE(RES);
    
      END;
    
    Copy
  8. O consumidor chama consumer.create_template_request com o modelo combinado. Coloque o código que você fornece para o procedimento armazenado entre aspas simples (' '), em vez dos delimitadores de cifrões duplos ($$...$$), no argumento template_definition. Por exemplo:

    CALL samooha_by_snowflake_local_db.consumer.create_template_request(
      $cleanroom_name,
      $template_name,
      '
    BEGIN
    
    -- First, define the Python UDF.
    CREATE OR REPLACE FUNCTION CLEANROOM.custom_compare(min_status STRING, max_status STRING, this_status STRING)
    RETURNS boolean
    LANGUAGE PYTHON
    RUNTIME_VERSION = 3.10
    PACKAGES = (\'numpy\')
    
    HANDLER = \'custom_compare\'
    AS $$
    import numpy as np
    
    def custom_compare(min_status:str, max_status:str, this_status:str):
      statuses = [\'MEMBER\', \'SILVER\', \'GOLD\', \'PLATINUM\']
      return ((statuses.index(this_status) >= statuses.index(min_status)) &
              (statuses.index(this_status) <= statuses.index(max_status)))
        $$;
    
    -- Then define and execute the SQL query.
    LET SQL_TEXT varchar := $$
    SELECT
      c.status,
      c.hashed_email
    FROM IDENTIFIER( {{ my_table[0] }} ) as c
    WHERE cleanroom.custom_compare({{ min_status }}, {{ max_status }}, c.status);
    $$;
    
    -- Execute the query and then return the result.
    LET RES resultset := (EXECUTE IMMEDIATE :SQL_TEXT);
    RETURN TABLE(RES);
    
    END;
    ');
    
    Copy
  9. O consumidor e o provedor continuam com o fluxo do modelo definido pelo consumidor padrão:

    1. O provedor visualiza a solicitação de modelo (provider.list_pending_template_requests) e chama approve_template_request para aprová-la. Na solicitação, o provedor pode ver o modelo e o código no pacote.

    2. O consumidor verifica o status da solicitação (consumer.list_template_requests) e, quando o status é APPROVED, executa o modelo (consumer.run_analysis).

Exemplos de código escrito pelo consumidor

Os exemplos a seguir demonstram como adicionar UDFs escritas pelo provedor a uma sala limpa.

Baixe os exemplos a seguir e carregue-os como arquivos de planilha em sua conta Snowflake. Você precisa de contas separadas para o provedor e o consumidor, cada uma com a API de salas limpas instalada. Substitua as informações conforme indicado nos arquivos de amostra: