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.
- clarifai/__init__.py +1 -1
- clarifai/cli/model.py +105 -38
- clarifai/cli/pipeline.py +84 -6
- clarifai/cli/templates/model_templates.py +1 -1
- clarifai/client/base.py +54 -16
- clarifai/client/dataset.py +18 -6
- clarifai/client/model.py +23 -13
- clarifai/client/model_client.py +2 -0
- clarifai/client/module.py +14 -13
- clarifai/client/nodepool.py +3 -1
- clarifai/client/pipeline.py +23 -23
- clarifai/client/pipeline_step.py +20 -18
- clarifai/client/search.py +35 -11
- clarifai/client/user.py +180 -5
- clarifai/client/workflow.py +18 -17
- clarifai/runners/models/model_builder.py +149 -17
- clarifai/runners/pipeline_steps/pipeline_step_builder.py +97 -1
- clarifai/runners/pipelines/pipeline_builder.py +196 -34
- clarifai/runners/server.py +1 -0
- clarifai/runners/utils/code_script.py +12 -1
- clarifai/utils/cli.py +62 -0
- clarifai/utils/constants.py +5 -3
- clarifai/utils/hashing.py +117 -0
- clarifai/utils/secrets.py +7 -2
- {clarifai-11.8.1.dist-info → clarifai-11.8.3.dist-info}/METADATA +4 -3
- {clarifai-11.8.1.dist-info → clarifai-11.8.3.dist-info}/RECORD +30 -29
- {clarifai-11.8.1.dist-info → clarifai-11.8.3.dist-info}/WHEEL +0 -0
- {clarifai-11.8.1.dist-info → clarifai-11.8.3.dist-info}/entry_points.txt +0 -0
- {clarifai-11.8.1.dist-info → clarifai-11.8.3.dist-info}/licenses/LICENSE +0 -0
- {clarifai-11.8.1.dist-info → clarifai-11.8.3.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
904
|
-
|
905
|
-
|
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
|
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
|
"""
|