clarifai 11.0.3__py3-none-any.whl → 11.0.5__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/runners/models/model_upload.py +54 -37
- clarifai/runners/utils/loader.py +48 -12
- clarifai/utils/logging.py +7 -0
- {clarifai-11.0.3.dist-info → clarifai-11.0.5.dist-info}/METADATA +1 -1
- {clarifai-11.0.3.dist-info → clarifai-11.0.5.dist-info}/RECORD +10 -10
- {clarifai-11.0.3.dist-info → clarifai-11.0.5.dist-info}/LICENSE +0 -0
- {clarifai-11.0.3.dist-info → clarifai-11.0.5.dist-info}/WHEEL +0 -0
- {clarifai-11.0.3.dist-info → clarifai-11.0.5.dist-info}/entry_points.txt +0 -0
- {clarifai-11.0.3.dist-info → clarifai-11.0.5.dist-info}/top_level.txt +0 -0
clarifai/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "11.0.
|
1
|
+
__version__ = "11.0.5"
|
@@ -29,16 +29,21 @@ def _clear_line(n: int = 1) -> None:
|
|
29
29
|
|
30
30
|
class ModelUploader:
|
31
31
|
|
32
|
-
def __init__(self, folder: str, validate_api_ids: bool = True):
|
32
|
+
def __init__(self, folder: str, validate_api_ids: bool = True, download_validation_only=False):
|
33
33
|
"""
|
34
34
|
:param folder: The folder containing the model.py, config.yaml, requirements.txt and
|
35
35
|
checkpoints.
|
36
|
-
:param validate_api_ids: Whether to validate the user_id and app_id in the config file.
|
36
|
+
:param validate_api_ids: Whether to validate the user_id and app_id in the config file. TODO(zeiler):
|
37
|
+
deprecate in favor of download_validation_only.
|
38
|
+
:param download_validation_only: Whether to skip the API config validation. Set to True if
|
39
|
+
just downloading a checkpoint.
|
37
40
|
"""
|
38
41
|
self._client = None
|
42
|
+
if not validate_api_ids: # for backwards compatibility
|
43
|
+
download_validation_only = True
|
44
|
+
self.download_validation_only = download_validation_only
|
39
45
|
self.folder = self._validate_folder(folder)
|
40
46
|
self.config = self._load_config(os.path.join(self.folder, 'config.yaml'))
|
41
|
-
self.validate_api_ids = validate_api_ids
|
42
47
|
self._validate_config()
|
43
48
|
self.model_proto = self._get_model_proto()
|
44
49
|
self.model_id = self.model_proto.id
|
@@ -46,19 +51,23 @@ class ModelUploader:
|
|
46
51
|
self.inference_compute_info = self._get_inference_compute_info()
|
47
52
|
self.is_v3 = True # Do model build for v3
|
48
53
|
|
49
|
-
|
50
|
-
|
54
|
+
def _validate_folder(self, folder):
|
55
|
+
if folder == ".":
|
56
|
+
folder = "" # will getcwd() next which ends with /
|
51
57
|
if not folder.startswith("/"):
|
52
58
|
folder = os.path.join(os.getcwd(), folder)
|
53
59
|
logger.info(f"Validating folder: {folder}")
|
54
60
|
if not os.path.exists(folder):
|
55
61
|
raise FileNotFoundError(f"Folder {folder} not found, please provide a valid folder path")
|
56
62
|
files = os.listdir(folder)
|
57
|
-
assert "requirements.txt" in files, "requirements.txt not found in the folder"
|
58
63
|
assert "config.yaml" in files, "config.yaml not found in the folder"
|
64
|
+
# If just downloading we don't need requirements.txt or the python code, we do need the
|
65
|
+
# 1/ folder to put 1/checkpoints into.
|
59
66
|
assert "1" in files, "Subfolder '1' not found in the folder"
|
60
|
-
|
61
|
-
|
67
|
+
if not self.download_validation_only:
|
68
|
+
assert "requirements.txt" in files, "requirements.txt not found in the folder"
|
69
|
+
subfolder_files = os.listdir(os.path.join(folder, '1'))
|
70
|
+
assert 'model.py' in subfolder_files, "model.py not found in the folder"
|
62
71
|
return folder
|
63
72
|
|
64
73
|
@staticmethod
|
@@ -68,22 +77,27 @@ class ModelUploader:
|
|
68
77
|
return config
|
69
78
|
|
70
79
|
def _validate_config_checkpoints(self):
|
71
|
-
|
80
|
+
"""
|
81
|
+
Validates the checkpoints section in the config file.
|
82
|
+
:return: loader_type the type of loader or None if no checkpoints.
|
83
|
+
:return: repo_id location of checkpoint.
|
84
|
+
:return: hf_token token to access checkpoint.
|
85
|
+
"""
|
72
86
|
assert "type" in self.config.get("checkpoints"), "No loader type specified in the config file"
|
73
87
|
loader_type = self.config.get("checkpoints").get("type")
|
74
88
|
if not loader_type:
|
75
89
|
logger.info("No loader type specified in the config file for checkpoints")
|
90
|
+
return None, None, None
|
76
91
|
assert loader_type == "huggingface", "Only huggingface loader supported for now"
|
77
92
|
if loader_type == "huggingface":
|
78
93
|
assert "repo_id" in self.config.get("checkpoints"), "No repo_id specified in the config file"
|
79
94
|
repo_id = self.config.get("checkpoints").get("repo_id")
|
80
95
|
|
81
|
-
|
82
|
-
|
96
|
+
# get from config.yaml otherwise fall back to HF_TOKEN env var.
|
97
|
+
hf_token = self.config.get("checkpoints").get("hf_token", os.environ.get("HF_TOKEN", None))
|
98
|
+
return loader_type, repo_id, hf_token
|
83
99
|
|
84
100
|
def _check_app_exists(self):
|
85
|
-
if not self.validate_api_ids:
|
86
|
-
return True
|
87
101
|
resp = self.client.STUB.GetApp(service_pb2.GetAppRequest(user_app_id=self.client.user_app_id))
|
88
102
|
if resp.status.code == status_code_pb2.SUCCESS:
|
89
103
|
return True
|
@@ -113,21 +127,19 @@ class ModelUploader:
|
|
113
127
|
sys.exit(1)
|
114
128
|
|
115
129
|
def _validate_config(self):
|
116
|
-
self.
|
130
|
+
if not self.download_validation_only:
|
131
|
+
self._validate_config_model()
|
117
132
|
|
118
|
-
|
119
|
-
self._validate_config_checkpoints()
|
133
|
+
assert "inference_compute_info" in self.config, "inference_compute_info not found in the config file"
|
120
134
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
model_type_id = self.config.get('model').get('model_type_id')
|
125
|
-
assert model_type_id in CONCEPTS_REQUIRED_MODEL_TYPE, f"Model type {model_type_id} not supported for concepts"
|
135
|
+
if self.config.get("concepts"):
|
136
|
+
model_type_id = self.config.get('model').get('model_type_id')
|
137
|
+
assert model_type_id in CONCEPTS_REQUIRED_MODEL_TYPE, f"Model type {model_type_id} not supported for concepts"
|
126
138
|
|
127
139
|
if self.config.get("checkpoints"):
|
128
|
-
_, hf_token = self._validate_config_checkpoints()
|
140
|
+
loader_type, _, hf_token = self._validate_config_checkpoints()
|
129
141
|
|
130
|
-
if hf_token:
|
142
|
+
if loader_type == "huggingface" and hf_token:
|
131
143
|
is_valid_token = HuggingFaceLoader.validate_hftoken(hf_token)
|
132
144
|
if not is_valid_token:
|
133
145
|
logger.error(
|
@@ -311,16 +323,19 @@ class ModelUploader:
|
|
311
323
|
logger.info("No checkpoints specified in the config file")
|
312
324
|
return True
|
313
325
|
|
314
|
-
repo_id, hf_token = self._validate_config_checkpoints()
|
326
|
+
loader_type, repo_id, hf_token = self._validate_config_checkpoints()
|
315
327
|
|
316
|
-
|
317
|
-
|
328
|
+
success = True
|
329
|
+
if loader_type == "huggingface":
|
330
|
+
loader = HuggingFaceLoader(repo_id=repo_id, token=hf_token)
|
331
|
+
success = loader.download_checkpoints(self.checkpoint_path)
|
318
332
|
|
319
|
-
if
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
333
|
+
if loader_type:
|
334
|
+
if not success:
|
335
|
+
logger.error(f"Failed to download checkpoints for model {repo_id}")
|
336
|
+
sys.exit(1)
|
337
|
+
else:
|
338
|
+
logger.info(f"Downloaded checkpoints for model {repo_id}")
|
324
339
|
return success
|
325
340
|
|
326
341
|
def _concepts_protos_from_concepts(self, concepts):
|
@@ -400,9 +415,10 @@ class ModelUploader:
|
|
400
415
|
input(
|
401
416
|
"Press Enter to download the HuggingFace model's config.json file to infer the concepts and continue..."
|
402
417
|
)
|
403
|
-
repo_id, hf_token = self._validate_config_checkpoints()
|
404
|
-
|
405
|
-
|
418
|
+
loader_type, repo_id, hf_token = self._validate_config_checkpoints()
|
419
|
+
if loader_type == "huggingface":
|
420
|
+
loader = HuggingFaceLoader(repo_id=repo_id, token=hf_token)
|
421
|
+
loader.download_config(self.checkpoint_path)
|
406
422
|
|
407
423
|
else:
|
408
424
|
logger.error(
|
@@ -413,10 +429,10 @@ class ModelUploader:
|
|
413
429
|
model_version_proto = self.get_model_version_proto()
|
414
430
|
|
415
431
|
if download_checkpoints:
|
416
|
-
tar_cmd = f"tar --exclude=*~ -czvf {self.tar_file} -C {self.folder} ."
|
432
|
+
tar_cmd = f"tar --exclude=*~ --exclude={self.tar_file} -czvf {self.tar_file} -C {self.folder} ."
|
417
433
|
else: # we don't want to send the checkpoints up even if they are in the folder.
|
418
434
|
logger.info(f"Skipping {self.checkpoint_path} in the tar file that is uploaded.")
|
419
|
-
tar_cmd = f"tar --exclude={self.checkpoint_suffix} --exclude=*~ -czvf {self.tar_file} -C {self.folder} ."
|
435
|
+
tar_cmd = f"tar --exclude={self.checkpoint_suffix} --exclude=*~ --exclude={self.tar_file} -czvf {self.tar_file} -C {self.folder} ."
|
420
436
|
# Tar the folder
|
421
437
|
logger.debug(tar_cmd)
|
422
438
|
os.system(tar_cmd)
|
@@ -493,7 +509,7 @@ class ModelUploader:
|
|
493
509
|
file_size = os.path.getsize(file_path)
|
494
510
|
logger.info(f"Uploading model version of model {self.model_proto.id}")
|
495
511
|
logger.info(f"Using file '{os.path.basename(file_path)}' of size: {file_size} bytes")
|
496
|
-
|
512
|
+
result = service_pb2.PostModelVersionsUploadRequest(
|
497
513
|
upload_config=service_pb2.PostModelVersionsUploadConfig(
|
498
514
|
user_app_id=self.client.user_app_id,
|
499
515
|
model_id=self.model_proto.id,
|
@@ -501,6 +517,7 @@ class ModelUploader:
|
|
501
517
|
total_size=file_size,
|
502
518
|
is_v3=self.is_v3,
|
503
519
|
))
|
520
|
+
return result
|
504
521
|
|
505
522
|
def get_model_build_logs(self):
|
506
523
|
logs_request = service_pb2.ListLogEntriesRequest(
|
clarifai/runners/utils/loader.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
import fnmatch
|
1
2
|
import importlib.util
|
2
3
|
import json
|
3
4
|
import os
|
5
|
+
import shutil
|
4
6
|
import subprocess
|
5
7
|
|
6
8
|
from clarifai.utils.logging import logger
|
@@ -39,7 +41,7 @@ class HuggingFaceLoader:
|
|
39
41
|
def download_checkpoints(self, checkpoint_path: str):
|
40
42
|
# throw error if huggingface_hub wasn't installed
|
41
43
|
try:
|
42
|
-
from huggingface_hub import
|
44
|
+
from huggingface_hub import snapshot_download
|
43
45
|
except ImportError:
|
44
46
|
raise ImportError(self.HF_DOWNLOAD_TEXT)
|
45
47
|
if os.path.exists(checkpoint_path) and self.validate_download(checkpoint_path):
|
@@ -53,16 +55,17 @@ class HuggingFaceLoader:
|
|
53
55
|
logger.error("Model %s not found on Hugging Face" % (self.repo_id))
|
54
56
|
return False
|
55
57
|
|
56
|
-
ignore_patterns =
|
57
|
-
repo_files = list_repo_files(repo_id=self.repo_id, token=self.token)
|
58
|
-
if any(f.endswith(".safetensors") for f in repo_files):
|
59
|
-
logger.info(f"SafeTensors found in {self.repo_id}, downloading only .safetensors files.")
|
60
|
-
ignore_patterns = ["original/*", "*.pth", "*.bin"]
|
58
|
+
self.ignore_patterns = self._get_ignore_patterns()
|
61
59
|
snapshot_download(
|
62
60
|
repo_id=self.repo_id,
|
63
61
|
local_dir=checkpoint_path,
|
64
62
|
local_dir_use_symlinks=False,
|
65
|
-
ignore_patterns=ignore_patterns)
|
63
|
+
ignore_patterns=self.ignore_patterns)
|
64
|
+
# Remove the `.cache` folder if it exists
|
65
|
+
cache_path = os.path.join(checkpoint_path, ".cache")
|
66
|
+
if os.path.exists(cache_path) and os.path.isdir(cache_path):
|
67
|
+
shutil.rmtree(cache_path)
|
68
|
+
|
66
69
|
except Exception as e:
|
67
70
|
logger.error(f"Error downloading model checkpoints {e}")
|
68
71
|
return False
|
@@ -109,11 +112,44 @@ class HuggingFaceLoader:
|
|
109
112
|
from huggingface_hub import list_repo_files
|
110
113
|
except ImportError:
|
111
114
|
raise ImportError(self.HF_DOWNLOAD_TEXT)
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
115
|
+
# Get the list of files on the repo
|
116
|
+
repo_files = list_repo_files(self.repo_id, token=self.token)
|
117
|
+
|
118
|
+
self.ignore_patterns = self._get_ignore_patterns()
|
119
|
+
# Get the list of files on the repo that are not ignored
|
120
|
+
if getattr(self, "ignore_patterns", None):
|
121
|
+
patterns = self.ignore_patterns
|
122
|
+
|
123
|
+
def should_ignore(file_path):
|
124
|
+
return any(fnmatch.fnmatch(file_path, pattern) for pattern in patterns)
|
125
|
+
|
126
|
+
repo_files = [f for f in repo_files if not should_ignore(f)]
|
127
|
+
|
128
|
+
# Check if downloaded files match the files we expect (ignoring ignored patterns)
|
129
|
+
checkpoint_dir_files = []
|
130
|
+
for dp, dn, fn in os.walk(os.path.expanduser(checkpoint_path)):
|
131
|
+
checkpoint_dir_files.extend(
|
132
|
+
[os.path.relpath(os.path.join(dp, f), checkpoint_path) for f in fn])
|
133
|
+
|
134
|
+
# Validate by comparing file lists
|
135
|
+
return len(checkpoint_dir_files) >= len(repo_files) and not (
|
136
|
+
len(set(repo_files) - set(checkpoint_dir_files)) > 0) and len(repo_files) > 0
|
137
|
+
|
138
|
+
def _get_ignore_patterns(self):
|
139
|
+
# check if model exists on HF
|
140
|
+
try:
|
141
|
+
from huggingface_hub import list_repo_files
|
142
|
+
except ImportError:
|
143
|
+
raise ImportError(self.HF_DOWNLOAD_TEXT)
|
144
|
+
|
145
|
+
# Get the list of files on the repo that are not ignored
|
146
|
+
repo_files = list_repo_files(self.repo_id, token=self.token)
|
147
|
+
self.ignore_patterns = None
|
148
|
+
if any(f.endswith(".safetensors") for f in repo_files):
|
149
|
+
self.ignore_patterns = [
|
150
|
+
"**/original/*", "**/*.pth", "**/*.bin", "*.pth", "*.bin", "**/.cache/*"
|
151
|
+
]
|
152
|
+
return self.ignore_patterns
|
117
153
|
|
118
154
|
@staticmethod
|
119
155
|
def validate_config(checkpoint_path: str):
|
clarifai/utils/logging.py
CHANGED
@@ -282,6 +282,11 @@ class JsonFormatter(logging.Formatter):
|
|
282
282
|
except Exception:
|
283
283
|
self.source_host = ""
|
284
284
|
|
285
|
+
self.extra_blacklist_fields = []
|
286
|
+
extra_blacklist_fields = os.getenv('EXTRA_JSON_LOGGER_BLACKLIST_FIELDS', None)
|
287
|
+
if extra_blacklist_fields:
|
288
|
+
self.extra_blacklist_fields = extra_blacklist_fields.split(",")
|
289
|
+
|
285
290
|
def _build_fields(self, defaults, fields):
|
286
291
|
"""Return provided fields including any in defaults
|
287
292
|
"""
|
@@ -302,6 +307,8 @@ class JsonFormatter(logging.Formatter):
|
|
302
307
|
msg = record.getMessage()
|
303
308
|
for k in FIELD_BLACKLIST:
|
304
309
|
fields.pop(k, None)
|
310
|
+
for k in self.extra_blacklist_fields:
|
311
|
+
fields.pop(k, None)
|
305
312
|
# Rename 'levelname' to 'level' and make the value lowercase to match Go logs
|
306
313
|
level = fields.pop('levelname', None)
|
307
314
|
if level:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
clarifai/__init__.py,sha256=
|
1
|
+
clarifai/__init__.py,sha256=p5EKOiD3MZA1O2ik4FV32rDBe9Z8ERMDwHJDG9GNB-k,23
|
2
2
|
clarifai/cli.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
clarifai/errors.py,sha256=RwzTajwds51wLD0MVlMC5kcpBnzRpreDLlazPSBZxrg,2605
|
4
4
|
clarifai/versions.py,sha256=jctnczzfGk_S3EnVqb2FjRKfSREkNmvNEwAAa_VoKiQ,222
|
@@ -69,19 +69,19 @@ clarifai/runners/models/model_class.py,sha256=9JSPAr4U4K7xI0kSl-q0mHB06zknm2OR-8
|
|
69
69
|
clarifai/runners/models/model_run_locally.py,sha256=OhzQbmaV8Wwgs2H0KhdDF6Z7bYSaIh4RRA0QwSiv5vY,20644
|
70
70
|
clarifai/runners/models/model_runner.py,sha256=3vzoastQxkGRDK8T9aojDsLNBb9A3IiKm6YmbFrE9S0,6241
|
71
71
|
clarifai/runners/models/model_servicer.py,sha256=X4715PVA5PBurRTYcwSEudg8fShGV6InAF4mmRlRcHg,2826
|
72
|
-
clarifai/runners/models/model_upload.py,sha256=
|
72
|
+
clarifai/runners/models/model_upload.py,sha256=Mxms6hg_u62p_WvuoTUOJ5QFNugWkmt7vnrk5dJnqg8,23927
|
73
73
|
clarifai/runners/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
74
74
|
clarifai/runners/utils/const.py,sha256=eyBrj5ywuGKPF-IFipm7yjiYyLhnsKhMNZ6xF-OvykQ,1250
|
75
75
|
clarifai/runners/utils/data_handler.py,sha256=sxy9zlAgI6ETuxCQhUgEXAn2GCsaW1GxpK6GTaMne0g,6966
|
76
76
|
clarifai/runners/utils/data_utils.py,sha256=R1iQ82TuQ9JwxCJk8yEB1Lyb0BYVhVbWJI9YDi1zGOs,318
|
77
|
-
clarifai/runners/utils/loader.py,sha256=
|
77
|
+
clarifai/runners/utils/loader.py,sha256=Q4psyvHjGPDHA6GMFLEhSSVqPRVq4vJyYyDItKn1WwU,6100
|
78
78
|
clarifai/runners/utils/url_fetcher.py,sha256=v_8JOWmkyFAzsBulsieKX7Nfjy1Yg7wGSZeqfEvw2cg,1640
|
79
79
|
clarifai/schema/search.py,sha256=JjTi8ammJgZZ2OGl4K6tIA4zEJ1Fr2ASZARXavI1j5c,2448
|
80
80
|
clarifai/urls/helper.py,sha256=tjoMGGHuWX68DUB0pk4MEjrmFsClUAQj2jmVEM_Sy78,4751
|
81
81
|
clarifai/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
82
82
|
clarifai/utils/cli.py,sha256=O6ukcQb05pFIgdQKWn0tL0AALAjT3U3DFRjd_GgPCvk,1918
|
83
83
|
clarifai/utils/constants.py,sha256=MG_iHnSwNEyUZOpvsrTicNwaT4CIjmlK_Ixk_qqEX8g,142
|
84
|
-
clarifai/utils/logging.py,sha256=
|
84
|
+
clarifai/utils/logging.py,sha256=Co2J4JzltrVEf-Fbll3X1n-iVxYOGXbfLVwx-X49bMg,11855
|
85
85
|
clarifai/utils/misc.py,sha256=ptjt1NtteDT0EhrPoyQ7mgWtvoAQ-XNncQaZvNHb0KI,2253
|
86
86
|
clarifai/utils/model_train.py,sha256=Mndqy5GNu7kjQHjDyNVyamL0hQFLGSHcWhOuPyOvr1w,8005
|
87
87
|
clarifai/utils/evaluation/__init__.py,sha256=PYkurUrXrGevByj7RFb6CoU1iC7fllyQSfnnlo9WnY8,69
|
@@ -92,9 +92,9 @@ clarifai/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
92
92
|
clarifai/workflows/export.py,sha256=vICRhIreqDSShxLKjHNM2JwzKsf1B4fdXB0ciMcA70k,1945
|
93
93
|
clarifai/workflows/utils.py,sha256=nGeB_yjVgUO9kOeKTg4OBBaBz-AwXI3m-huSVj-9W18,1924
|
94
94
|
clarifai/workflows/validate.py,sha256=yJq03MaJqi5AK3alKGJJBR89xmmjAQ31sVufJUiOqY8,2556
|
95
|
-
clarifai-11.0.
|
96
|
-
clarifai-11.0.
|
97
|
-
clarifai-11.0.
|
98
|
-
clarifai-11.0.
|
99
|
-
clarifai-11.0.
|
100
|
-
clarifai-11.0.
|
95
|
+
clarifai-11.0.5.dist-info/LICENSE,sha256=mUqF_d12-qE2n41g7C5_sq-BMLOcj6CNN-jevr15YHU,555
|
96
|
+
clarifai-11.0.5.dist-info/METADATA,sha256=SDCVegVMwwCs-h_cv6WgK5LBfWoNmjsKLWH8uRrs2q8,22456
|
97
|
+
clarifai-11.0.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
98
|
+
clarifai-11.0.5.dist-info/entry_points.txt,sha256=X9FZ4Z-i_r2Ud1RpZ9sNIFYuu_-9fogzCMCRUD9hyX0,51
|
99
|
+
clarifai-11.0.5.dist-info/top_level.txt,sha256=wUMdCQGjkxaynZ6nZ9FAnvBUCgp5RJUVFSy2j-KYo0s,9
|
100
|
+
clarifai-11.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|