clarifai 11.8.1__py3-none-any.whl → 11.8.3__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.
@@ -102,6 +102,7 @@ class ModelBuilder:
102
102
  self.folder = self._validate_folder(folder)
103
103
  self.config = self._load_config(os.path.join(self.folder, 'config.yaml'))
104
104
  self._validate_config()
105
+ self._validate_config_secrets()
105
106
  self._validate_stream_options()
106
107
  self.model_proto = self._get_model_proto()
107
108
  self.model_id = self.model_proto.id
@@ -465,6 +466,115 @@ class ModelBuilder:
465
466
  "2) set_output_context"
466
467
  )
467
468
 
469
+ def _validate_config_secrets(self):
470
+ """
471
+ Validate the secrets section in the config file.
472
+ """
473
+ if "secrets" not in self.config:
474
+ return
475
+
476
+ secrets = self.config.get("secrets", [])
477
+ if not isinstance(secrets, list):
478
+ raise ValueError("The 'secrets' field must be an array.")
479
+
480
+ for i, secret in enumerate(secrets):
481
+ if not isinstance(secret, dict):
482
+ raise ValueError(f"Secret at index {i} must be a dictionary.")
483
+
484
+ # Validate required fields
485
+ if "id" not in secret or not secret["id"]:
486
+ raise ValueError(f"Secret at index {i} must have a non-empty 'id' field.")
487
+
488
+ if "type" not in secret or not secret["type"]:
489
+ secret["type"] = "env"
490
+
491
+ if "env_var" not in secret or not secret["env_var"]:
492
+ raise ValueError(f"Secret at index {i} must have a non-empty 'env_var' field.")
493
+ # Validate secret type
494
+ if secret["type"] != "env":
495
+ raise ValueError(
496
+ f"Secret at index {i} has invalid type '{secret['type']}'. Must be 'env'."
497
+ )
498
+
499
+ logger.info(f"Validated {len(secrets)} secrets in config file.")
500
+
501
+ def _process_secrets(self):
502
+ """
503
+ Process secrets from config file and create/validate them using the User client.
504
+ Returns the processed secrets array for inclusion in ModelVersion.OutputInfo.Params.
505
+ """
506
+ if "secrets" not in self.config:
507
+ return []
508
+
509
+ secrets = self.config.get("secrets", [])
510
+ if not secrets:
511
+ return []
512
+
513
+ # Get user client for secret operations
514
+ user = User(
515
+ user_id=self.config.get('model').get('user_id'),
516
+ pat=self.client.pat,
517
+ token=self.client.token,
518
+ base_url=self.client.base,
519
+ )
520
+
521
+ processed_secrets = []
522
+ secrets_to_create = []
523
+
524
+ for secret in secrets:
525
+ secret_id = secret["id"]
526
+ secret_type = secret.get("type", "env")
527
+ env_var = secret["env_var"]
528
+ secret_value = secret.get("value") # Optional for existing secrets
529
+
530
+ # Check if secret already exists
531
+ try:
532
+ existing_secret = user.get_secret(secret_id)
533
+ logger.info(f"Secret '{secret_id}' already exists, using existing secret.")
534
+
535
+ # Add to processed secrets without the value
536
+ processed_secret = {
537
+ "id": secret_id,
538
+ "type": secret_type,
539
+ "env_var": env_var,
540
+ }
541
+ processed_secrets.append(processed_secret)
542
+
543
+ except Exception:
544
+ # Secret doesn't exist, need to create it
545
+ if secret_value:
546
+ logger.info(f"Secret '{secret_id}' does not exist, will create it.")
547
+ secrets_to_create.append(
548
+ {
549
+ "id": secret_id,
550
+ "value": secret_value,
551
+ "description": secret.get("description", f"Secret for {env_var}"),
552
+ }
553
+ )
554
+
555
+ # Add to processed secrets
556
+ processed_secret = {
557
+ "id": secret_id,
558
+ "type": secret_type,
559
+ "env_var": env_var,
560
+ }
561
+ processed_secrets.append(processed_secret)
562
+ else:
563
+ raise ValueError(
564
+ f"Secret '{secret_id}' does not exist and no value provided for creation."
565
+ )
566
+
567
+ # Create new secrets if any
568
+ if secrets_to_create:
569
+ try:
570
+ created_secrets = user.create_secrets(secrets_to_create)
571
+ logger.info(f"Successfully created {len(created_secrets)} new secrets.")
572
+ except Exception as e:
573
+ logger.error(f"Failed to create secrets: {e}")
574
+ raise
575
+
576
+ return processed_secrets
577
+
468
578
  def _is_clarifai_internal(self):
469
579
  """
470
580
  Check if the current user is a Clarifai internal user based on email domain.
@@ -891,19 +1001,21 @@ class ModelBuilder:
891
1001
  )
892
1002
  torch_version = dependencies.get('torch', None)
893
1003
  if 'torch' in dependencies:
894
- if python_version != DEFAULT_PYTHON_VERSION:
895
- raise Exception(
896
- f"torch is not supported with Python version {python_version}, please use Python version {DEFAULT_PYTHON_VERSION} in your config.yaml"
897
- )
898
1004
  if not torch_version:
899
1005
  logger.info(
900
1006
  f"Setup: torch version not found in requirements.txt, using the default version {DEFAULT_AMD_TORCH_VERSION}"
901
1007
  )
902
1008
  torch_version = DEFAULT_AMD_TORCH_VERSION
903
- if torch_version not in [DEFAULT_AMD_TORCH_VERSION]:
904
- raise Exception(
905
- f"torch version {torch_version} not supported, please use one of the following versions: {DEFAULT_AMD_TORCH_VERSION} in your requirements.txt"
906
- )
1009
+ elif torch_version not in [DEFAULT_AMD_TORCH_VERSION]:
1010
+ # Currently, we have only one vLLM image built with the DEFAULT_AMD_TORCH_VERSION.
1011
+ # If the user requests a different PyTorch version, that specific version will be
1012
+ # installed during the requirements.txt installation step
1013
+ torch_version = DEFAULT_AMD_TORCH_VERSION
1014
+ else:
1015
+ logger.info(
1016
+ f"`torch` not found in requirements.txt, using the default torch=={DEFAULT_AMD_TORCH_VERSION}"
1017
+ )
1018
+ torch_version = DEFAULT_AMD_TORCH_VERSION
907
1019
  python_version = DEFAULT_PYTHON_VERSION
908
1020
  gpu_version = DEFAULT_AMD_GPU_VERSION
909
1021
  final_image = AMD_VLLM_BASE_IMAGE.format(
@@ -912,21 +1024,17 @@ class ModelBuilder:
912
1024
  gpu_version=gpu_version,
913
1025
  )
914
1026
  logger.info("Setup: Using vLLM base image to build the Docker image")
915
- elif 'torch' in dependencies:
1027
+ elif (
1028
+ 'torch' in dependencies
1029
+ and (dependencies['torch'] in [None, DEFAULT_AMD_TORCH_VERSION])
1030
+ and python_version == DEFAULT_PYTHON_VERSION
1031
+ ):
916
1032
  torch_version = dependencies['torch']
917
- if python_version != DEFAULT_PYTHON_VERSION:
918
- raise Exception(
919
- f"torch is not supported with Python version {python_version}, please use Python version {DEFAULT_PYTHON_VERSION} in your config.yaml"
920
- )
921
1033
  if not torch_version:
922
1034
  logger.info(
923
1035
  f"torch version not found in requirements.txt, using the default version {DEFAULT_AMD_TORCH_VERSION}"
924
1036
  )
925
1037
  torch_version = DEFAULT_AMD_TORCH_VERSION
926
- if torch_version not in [DEFAULT_AMD_TORCH_VERSION]:
927
- raise Exception(
928
- f"torch version {torch_version} not supported, please use one of the following versions: {DEFAULT_AMD_TORCH_VERSION} in your requirements.txt"
929
- )
930
1038
  python_version = DEFAULT_PYTHON_VERSION
931
1039
  gpu_version = DEFAULT_AMD_GPU_VERSION
932
1040
  final_image = AMD_TORCH_BASE_IMAGE.format(
@@ -1258,6 +1366,29 @@ class ModelBuilder:
1258
1366
  metadata_struct.update({'git_registry': git_info})
1259
1367
  model_version_proto.metadata.CopyFrom(metadata_struct)
1260
1368
 
1369
+ # Process and add secrets to output_info.params
1370
+ try:
1371
+ processed_secrets = self._process_secrets()
1372
+ if processed_secrets:
1373
+ # Initialize output_info.params if not already present
1374
+ if not model_version_proto.HasField("output_info"):
1375
+ model_version_proto.output_info.CopyFrom(resources_pb2.OutputInfo())
1376
+
1377
+ # Initialize params if not already present
1378
+ if not model_version_proto.output_info.HasField("params"):
1379
+ from google.protobuf.struct_pb2 import Struct
1380
+
1381
+ model_version_proto.output_info.params.CopyFrom(Struct())
1382
+
1383
+ # Add secrets to params
1384
+ model_version_proto.output_info.params.update({"secrets": processed_secrets})
1385
+ logger.info(
1386
+ f"Added {len(processed_secrets)} secrets to model version output_info.params"
1387
+ )
1388
+ except Exception as e:
1389
+ logger.error(f"Failed to process secrets: {e}")
1390
+ raise
1391
+
1261
1392
  model_type_id = self.config.get('model').get('model_type_id')
1262
1393
  if model_type_id in CONCEPTS_REQUIRED_MODEL_TYPE:
1263
1394
  if 'concepts' in self.config:
@@ -1382,6 +1513,7 @@ class ModelBuilder:
1382
1513
  user_id=self.client.user_app_id.user_id,
1383
1514
  app_id=self.client.user_app_id.app_id,
1384
1515
  model_id=self.model_proto.id,
1516
+ colorize=True,
1385
1517
  )
1386
1518
  logger.info("""\n
1387
1519
  XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
@@ -4,6 +4,7 @@ import sys
4
4
  import tarfile
5
5
  import time
6
6
  from string import Template
7
+ from typing import List, Optional
7
8
 
8
9
  import yaml
9
10
  from clarifai_grpc.grpc.api import resources_pb2, service_pb2
@@ -11,6 +12,7 @@ from clarifai_grpc.grpc.api.status import status_code_pb2
11
12
  from google.protobuf import json_format
12
13
 
13
14
  from clarifai.client.base import BaseClient
15
+ from clarifai.utils.hashing import hash_directory
14
16
  from clarifai.utils.logging import logger
15
17
  from clarifai.utils.misc import get_uuid
16
18
  from clarifai.versions import CLIENT_VERSION
@@ -22,12 +24,13 @@ UPLOAD_CHUNK_SIZE = 14 * 1024 * 1024
22
24
  class PipelineStepBuilder:
23
25
  """Pipeline Step Builder class for managing pipeline step upload to Clarifai."""
24
26
 
25
- def __init__(self, folder: str):
27
+ def __init__(self, folder: str, hash_exclusions: Optional[List[str]] = None):
26
28
  """
27
29
  Initialize PipelineStepBuilder.
28
30
 
29
31
  :param folder: The folder containing the pipeline step files (config.yaml, requirements.txt,
30
32
  dockerfile, and pipeline_step.py in 1/ subdirectory)
33
+ :param hash_exclusions: List of file names to exclude from hash calculation (defaults to ['config-lock.yaml'])
31
34
  """
32
35
  self._client = None
33
36
  self.folder = self._validate_folder(folder)
@@ -37,6 +40,10 @@ class PipelineStepBuilder:
37
40
  self.pipeline_step_id = self.pipeline_step_proto.id
38
41
  self.pipeline_step_version_id = None
39
42
  self.pipeline_step_compute_info = self._get_pipeline_step_compute_info()
43
+ # Configure files to exclude from hash calculation
44
+ self.hash_exclusions = (
45
+ hash_exclusions if hash_exclusions is not None else ['config-lock.yaml']
46
+ )
40
47
 
41
48
  @property
42
49
  def client(self):
@@ -490,6 +497,95 @@ COPY --link=true requirements.txt config.yaml /home/nonroot/main/
490
497
 
491
498
  raise TimeoutError("Pipeline step build did not finish in time")
492
499
 
500
+ def load_config_lock(self):
501
+ """
502
+ Load existing config-lock.yaml if it exists.
503
+
504
+ :return: Dictionary with config-lock data or None if file doesn't exist
505
+ """
506
+ config_lock_path = os.path.join(self.folder, "config-lock.yaml")
507
+ if os.path.exists(config_lock_path):
508
+ try:
509
+ with open(config_lock_path, 'r', encoding='utf-8') as f:
510
+ return yaml.safe_load(f)
511
+ except Exception as e:
512
+ logger.warning(f"Failed to load config-lock.yaml: {e}")
513
+ return None
514
+ return None
515
+
516
+ def should_upload_step(self, algo="md5"):
517
+ """
518
+ Check if the pipeline step should be uploaded based on hash comparison.
519
+
520
+ :param algo: Hash algorithm to use
521
+ :return: True if step should be uploaded, False otherwise
522
+ """
523
+ config_lock = self.load_config_lock()
524
+
525
+ # If no config-lock.yaml exists, upload the step (first time upload)
526
+ if config_lock is None:
527
+ logger.info("No config-lock.yaml found, will upload pipeline step")
528
+ return True
529
+
530
+ # Compare stored hash with freshly computed one
531
+ current_hash = hash_directory(self.folder, algo=algo, exclude_files=self.hash_exclusions)
532
+ stored_hash_info = config_lock.get("hash", {})
533
+ stored_hash = stored_hash_info.get("value", "")
534
+ stored_algo = stored_hash_info.get("algo", "md5")
535
+
536
+ # If algorithm changed, re-upload to update hash
537
+ if stored_algo != algo:
538
+ logger.info(
539
+ f"Hash algorithm changed from {stored_algo} to {algo}, will upload pipeline step"
540
+ )
541
+ return True
542
+
543
+ # If hash changed, upload
544
+ if current_hash != stored_hash:
545
+ logger.info(
546
+ f"Hash changed (was: {stored_hash}, now: {current_hash}), will upload pipeline step"
547
+ )
548
+ return True
549
+
550
+ logger.info(f"Hash unchanged ({current_hash}), skipping pipeline step upload")
551
+ return False
552
+
553
+ def generate_config_lock(self, version_id, algo="md5"):
554
+ """
555
+ Generate config-lock.yaml content for the pipeline step.
556
+
557
+ :param version_id: Pipeline step version ID
558
+ :param algo: Hash algorithm used
559
+ :return: Dictionary with config-lock data
560
+ """
561
+ # Compute hash
562
+ hash_value = hash_directory(self.folder, algo=algo, exclude_files=self.hash_exclusions)
563
+
564
+ # Create config-lock structure
565
+ config_lock = {"id": version_id, "hash": {"algo": algo, "value": hash_value}}
566
+
567
+ # Append the original config.yaml contents
568
+ config_lock.update(self.config)
569
+
570
+ return config_lock
571
+
572
+ def save_config_lock(self, version_id, algo="md5"):
573
+ """
574
+ Save config-lock.yaml file with pipeline step metadata.
575
+
576
+ :param version_id: Pipeline step version ID
577
+ :param algo: Hash algorithm used
578
+ """
579
+ config_lock_data = self.generate_config_lock(version_id, algo)
580
+ config_lock_path = os.path.join(self.folder, "config-lock.yaml")
581
+
582
+ try:
583
+ with open(config_lock_path, 'w', encoding='utf-8') as f:
584
+ yaml.dump(config_lock_data, f, default_flow_style=False, allow_unicode=True)
585
+ logger.info(f"Generated config-lock.yaml at {config_lock_path}")
586
+ except Exception as e:
587
+ logger.error(f"Failed to save config-lock.yaml: {e}")
588
+
493
589
 
494
590
  def upload_pipeline_step(folder, skip_dockerfile=False):
495
591
  """