truefoundry 0.3.2__py3-none-any.whl → 0.4.0.dev0__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.

Potentially problematic release.


This version of truefoundry might be problematic. Click here for more details.

Files changed (225) hide show
  1. truefoundry/cli/__main__.py +3 -17
  2. truefoundry/common/__init__.py +0 -0
  3. truefoundry/common/request_utils.py +56 -0
  4. truefoundry/deploy/auto_gen/models.py +29 -1
  5. truefoundry/deploy/cli/cli.py +1 -1
  6. truefoundry/deploy/lib/auth/credential_provider.py +2 -12
  7. truefoundry/deploy/lib/clients/servicefoundry_client.py +0 -9
  8. truefoundry/deploy/lib/exceptions.py +1 -6
  9. truefoundry/deploy/lib/session.py +1 -16
  10. truefoundry/{python_deploy_codegen.py → deploy/python_deploy_codegen.py} +1 -0
  11. truefoundry/langchain/truefoundry_chat.py +1 -1
  12. truefoundry/langchain/truefoundry_embeddings.py +1 -1
  13. truefoundry/langchain/truefoundry_llm.py +1 -1
  14. truefoundry/langchain/utils.py +0 -41
  15. truefoundry/ml/__init__.py +46 -6
  16. truefoundry/ml/artifact/__init__.py +0 -0
  17. truefoundry/ml/artifact/truefoundry_artifact_repo.py +1120 -0
  18. truefoundry/ml/autogen/__init__.py +0 -0
  19. truefoundry/ml/autogen/client/__init__.py +373 -0
  20. truefoundry/ml/autogen/client/api/__init__.py +16 -0
  21. truefoundry/ml/autogen/client/api/auth_api.py +184 -0
  22. truefoundry/ml/autogen/client/api/deprecated_api.py +605 -0
  23. truefoundry/ml/autogen/client/api/experiments_api.py +2109 -0
  24. truefoundry/ml/autogen/client/api/health_api.py +299 -0
  25. truefoundry/ml/autogen/client/api/metrics_api.py +371 -0
  26. truefoundry/ml/autogen/client/api/mlfoundry_artifacts_api.py +7213 -0
  27. truefoundry/ml/autogen/client/api/python_deployment_config_api.py +201 -0
  28. truefoundry/ml/autogen/client/api/run_artifacts_api.py +231 -0
  29. truefoundry/ml/autogen/client/api/runs_api.py +2919 -0
  30. truefoundry/ml/autogen/client/api_client.py +822 -0
  31. truefoundry/ml/autogen/client/api_response.py +30 -0
  32. truefoundry/ml/autogen/client/configuration.py +489 -0
  33. truefoundry/ml/autogen/client/exceptions.py +161 -0
  34. truefoundry/ml/autogen/client/models/__init__.py +344 -0
  35. truefoundry/ml/autogen/client/models/add_custom_metrics_to_model_version_request_dto.py +69 -0
  36. truefoundry/ml/autogen/client/models/add_features_to_model_version_request_dto.py +83 -0
  37. truefoundry/ml/autogen/client/models/agent.py +125 -0
  38. truefoundry/ml/autogen/client/models/agent_app.py +118 -0
  39. truefoundry/ml/autogen/client/models/agent_open_api_tool.py +143 -0
  40. truefoundry/ml/autogen/client/models/agent_open_api_tool_with_fqn.py +144 -0
  41. truefoundry/ml/autogen/client/models/agent_with_fqn.py +127 -0
  42. truefoundry/ml/autogen/client/models/artifact_dto.py +115 -0
  43. truefoundry/ml/autogen/client/models/artifact_response_dto.py +75 -0
  44. truefoundry/ml/autogen/client/models/artifact_type.py +39 -0
  45. truefoundry/ml/autogen/client/models/artifact_version_dto.py +141 -0
  46. truefoundry/ml/autogen/client/models/artifact_version_response_dto.py +77 -0
  47. truefoundry/ml/autogen/client/models/artifact_version_status.py +35 -0
  48. truefoundry/ml/autogen/client/models/assistant_message.py +89 -0
  49. truefoundry/ml/autogen/client/models/authorize_user_for_model_request_dto.py +69 -0
  50. truefoundry/ml/autogen/client/models/authorize_user_for_model_version_request_dto.py +69 -0
  51. truefoundry/ml/autogen/client/models/backfill_default_storage_integration_id_request_dto.py +67 -0
  52. truefoundry/ml/autogen/client/models/blob_storage_reference.py +93 -0
  53. truefoundry/ml/autogen/client/models/body_get_search_runs_get.py +72 -0
  54. truefoundry/ml/autogen/client/models/chat_prompt.py +156 -0
  55. truefoundry/ml/autogen/client/models/chat_prompt_messages_inner.py +171 -0
  56. truefoundry/ml/autogen/client/models/columns_dto.py +73 -0
  57. truefoundry/ml/autogen/client/models/content.py +153 -0
  58. truefoundry/ml/autogen/client/models/content1.py +153 -0
  59. truefoundry/ml/autogen/client/models/content2.py +174 -0
  60. truefoundry/ml/autogen/client/models/content2_any_of_inner.py +150 -0
  61. truefoundry/ml/autogen/client/models/create_artifact_request_dto.py +74 -0
  62. truefoundry/ml/autogen/client/models/create_artifact_response_dto.py +66 -0
  63. truefoundry/ml/autogen/client/models/create_artifact_version_request_dto.py +74 -0
  64. truefoundry/ml/autogen/client/models/create_artifact_version_response_dto.py +66 -0
  65. truefoundry/ml/autogen/client/models/create_dataset_request_dto.py +76 -0
  66. truefoundry/ml/autogen/client/models/create_experiment_request_dto.py +94 -0
  67. truefoundry/ml/autogen/client/models/create_experiment_response_dto.py +67 -0
  68. truefoundry/ml/autogen/client/models/create_model_version_request_dto.py +95 -0
  69. truefoundry/ml/autogen/client/models/create_multi_part_upload_for_dataset_request_dto.py +73 -0
  70. truefoundry/ml/autogen/client/models/create_multi_part_upload_for_dataset_response_dto.py +79 -0
  71. truefoundry/ml/autogen/client/models/create_multi_part_upload_request_dto.py +73 -0
  72. truefoundry/ml/autogen/client/models/create_python_deployment_config_request_dto.py +72 -0
  73. truefoundry/ml/autogen/client/models/create_python_deployment_config_response_dto.py +68 -0
  74. truefoundry/ml/autogen/client/models/create_run_request_dto.py +97 -0
  75. truefoundry/ml/autogen/client/models/create_run_response_dto.py +76 -0
  76. truefoundry/ml/autogen/client/models/dataset_dto.py +112 -0
  77. truefoundry/ml/autogen/client/models/dataset_response_dto.py +75 -0
  78. truefoundry/ml/autogen/client/models/delete_artifact_versions_request_dto.py +65 -0
  79. truefoundry/ml/autogen/client/models/delete_dataset_request_dto.py +74 -0
  80. truefoundry/ml/autogen/client/models/delete_model_version_request_dto.py +65 -0
  81. truefoundry/ml/autogen/client/models/delete_run_request.py +65 -0
  82. truefoundry/ml/autogen/client/models/delete_tag_request_dto.py +68 -0
  83. truefoundry/ml/autogen/client/models/experiment_dto.py +127 -0
  84. truefoundry/ml/autogen/client/models/experiment_id_request_dto.py +67 -0
  85. truefoundry/ml/autogen/client/models/experiment_response_dto.py +75 -0
  86. truefoundry/ml/autogen/client/models/experiment_tag_dto.py +69 -0
  87. truefoundry/ml/autogen/client/models/feature_dto.py +68 -0
  88. truefoundry/ml/autogen/client/models/feature_value_type.py +35 -0
  89. truefoundry/ml/autogen/client/models/file_info_dto.py +76 -0
  90. truefoundry/ml/autogen/client/models/finalize_artifact_version_request_dto.py +101 -0
  91. truefoundry/ml/autogen/client/models/get_experiment_response_dto.py +88 -0
  92. truefoundry/ml/autogen/client/models/get_latest_run_log_response_dto.py +76 -0
  93. truefoundry/ml/autogen/client/models/get_metric_history_response.py +79 -0
  94. truefoundry/ml/autogen/client/models/get_signed_url_for_dataset_write_request_dto.py +68 -0
  95. truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_read_request_dto.py +68 -0
  96. truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_read_response_dto.py +81 -0
  97. truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_write_request_dto.py +69 -0
  98. truefoundry/ml/autogen/client/models/get_signed_urls_for_artifact_version_write_response_dto.py +83 -0
  99. truefoundry/ml/autogen/client/models/get_signed_urls_for_dataset_read_request_dto.py +68 -0
  100. truefoundry/ml/autogen/client/models/get_signed_urls_for_dataset_read_response_dto.py +81 -0
  101. truefoundry/ml/autogen/client/models/get_signed_urls_for_dataset_write_response_dto.py +81 -0
  102. truefoundry/ml/autogen/client/models/get_tenant_id_response_dto.py +74 -0
  103. truefoundry/ml/autogen/client/models/http_validation_error.py +82 -0
  104. truefoundry/ml/autogen/client/models/image_content_part.py +87 -0
  105. truefoundry/ml/autogen/client/models/image_url.py +75 -0
  106. truefoundry/ml/autogen/client/models/internal_metadata.py +180 -0
  107. truefoundry/ml/autogen/client/models/latest_run_log_dto.py +78 -0
  108. truefoundry/ml/autogen/client/models/list_artifact_versions_request_dto.py +107 -0
  109. truefoundry/ml/autogen/client/models/list_artifact_versions_response_dto.py +87 -0
  110. truefoundry/ml/autogen/client/models/list_artifacts_request_dto.py +96 -0
  111. truefoundry/ml/autogen/client/models/list_artifacts_response_dto.py +86 -0
  112. truefoundry/ml/autogen/client/models/list_colums_response_dto.py +75 -0
  113. truefoundry/ml/autogen/client/models/list_datasets_request_dto.py +78 -0
  114. truefoundry/ml/autogen/client/models/list_datasets_response_dto.py +86 -0
  115. truefoundry/ml/autogen/client/models/list_experiments_response_dto.py +86 -0
  116. truefoundry/ml/autogen/client/models/list_files_for_artifact_version_request_dto.py +76 -0
  117. truefoundry/ml/autogen/client/models/list_files_for_artifact_versions_response_dto.py +82 -0
  118. truefoundry/ml/autogen/client/models/list_files_for_dataset_request_dto.py +76 -0
  119. truefoundry/ml/autogen/client/models/list_files_for_dataset_response_dto.py +82 -0
  120. truefoundry/ml/autogen/client/models/list_latest_run_logs_response_dto.py +82 -0
  121. truefoundry/ml/autogen/client/models/list_metric_history_request_dto.py +69 -0
  122. truefoundry/ml/autogen/client/models/list_metric_history_response_dto.py +84 -0
  123. truefoundry/ml/autogen/client/models/list_model_version_response_dto.py +87 -0
  124. truefoundry/ml/autogen/client/models/list_model_versions_request_dto.py +93 -0
  125. truefoundry/ml/autogen/client/models/list_models_request_dto.py +89 -0
  126. truefoundry/ml/autogen/client/models/list_models_response_dto.py +84 -0
  127. truefoundry/ml/autogen/client/models/list_run_artifacts_response_dto.py +84 -0
  128. truefoundry/ml/autogen/client/models/list_run_logs_response_dto.py +82 -0
  129. truefoundry/ml/autogen/client/models/list_seed_experiments_response_dto.py +81 -0
  130. truefoundry/ml/autogen/client/models/log_batch_request_dto.py +106 -0
  131. truefoundry/ml/autogen/client/models/log_metric_request_dto.py +80 -0
  132. truefoundry/ml/autogen/client/models/log_param_request_dto.py +76 -0
  133. truefoundry/ml/autogen/client/models/method.py +37 -0
  134. truefoundry/ml/autogen/client/models/metric_collection_dto.py +82 -0
  135. truefoundry/ml/autogen/client/models/metric_dto.py +76 -0
  136. truefoundry/ml/autogen/client/models/mime_type.py +37 -0
  137. truefoundry/ml/autogen/client/models/model_configuration.py +103 -0
  138. truefoundry/ml/autogen/client/models/model_dto.py +122 -0
  139. truefoundry/ml/autogen/client/models/model_response_dto.py +75 -0
  140. truefoundry/ml/autogen/client/models/model_schema_dto.py +85 -0
  141. truefoundry/ml/autogen/client/models/model_version_dto.py +163 -0
  142. truefoundry/ml/autogen/client/models/model_version_response_dto.py +75 -0
  143. truefoundry/ml/autogen/client/models/multi_part_upload_dto.py +107 -0
  144. truefoundry/ml/autogen/client/models/multi_part_upload_response_dto.py +79 -0
  145. truefoundry/ml/autogen/client/models/multi_part_upload_storage_provider.py +34 -0
  146. truefoundry/ml/autogen/client/models/notify_artifact_version_failure_dto.py +65 -0
  147. truefoundry/ml/autogen/client/models/openapi_spec.py +152 -0
  148. truefoundry/ml/autogen/client/models/param_dto.py +66 -0
  149. truefoundry/ml/autogen/client/models/parameters.py +84 -0
  150. truefoundry/ml/autogen/client/models/prediction_type.py +34 -0
  151. truefoundry/ml/autogen/client/models/resolve_agent_app_response_dto.py +75 -0
  152. truefoundry/ml/autogen/client/models/restore_run_request_dto.py +65 -0
  153. truefoundry/ml/autogen/client/models/run_data_dto.py +104 -0
  154. truefoundry/ml/autogen/client/models/run_dto.py +84 -0
  155. truefoundry/ml/autogen/client/models/run_info_dto.py +105 -0
  156. truefoundry/ml/autogen/client/models/run_log_dto.py +90 -0
  157. truefoundry/ml/autogen/client/models/run_log_input_dto.py +80 -0
  158. truefoundry/ml/autogen/client/models/run_response_dto.py +75 -0
  159. truefoundry/ml/autogen/client/models/run_tag_dto.py +66 -0
  160. truefoundry/ml/autogen/client/models/search_runs_request_dto.py +94 -0
  161. truefoundry/ml/autogen/client/models/search_runs_response_dto.py +84 -0
  162. truefoundry/ml/autogen/client/models/set_experiment_tag_request_dto.py +73 -0
  163. truefoundry/ml/autogen/client/models/set_tag_request_dto.py +76 -0
  164. truefoundry/ml/autogen/client/models/signed_url_dto.py +69 -0
  165. truefoundry/ml/autogen/client/models/stop.py +152 -0
  166. truefoundry/ml/autogen/client/models/store_run_logs_request_dto.py +83 -0
  167. truefoundry/ml/autogen/client/models/system_message.py +89 -0
  168. truefoundry/ml/autogen/client/models/text.py +153 -0
  169. truefoundry/ml/autogen/client/models/text_content_part.py +84 -0
  170. truefoundry/ml/autogen/client/models/update_artifact_version_request_dto.py +74 -0
  171. truefoundry/ml/autogen/client/models/update_dataset_request_dto.py +74 -0
  172. truefoundry/ml/autogen/client/models/update_experiment_request_dto.py +74 -0
  173. truefoundry/ml/autogen/client/models/update_model_version_request_dto.py +93 -0
  174. truefoundry/ml/autogen/client/models/update_run_request_dto.py +78 -0
  175. truefoundry/ml/autogen/client/models/update_run_response_dto.py +76 -0
  176. truefoundry/ml/autogen/client/models/url.py +153 -0
  177. truefoundry/ml/autogen/client/models/user_message.py +89 -0
  178. truefoundry/ml/autogen/client/models/validation_error.py +87 -0
  179. truefoundry/ml/autogen/client/models/validation_error_loc_inner.py +154 -0
  180. truefoundry/ml/autogen/client/rest.py +426 -0
  181. truefoundry/ml/autogen/client_README.md +322 -0
  182. truefoundry/ml/cli/__init__.py +0 -0
  183. truefoundry/ml/cli/cli.py +18 -0
  184. truefoundry/ml/cli/commands/__init__.py +3 -0
  185. truefoundry/ml/cli/commands/download.py +87 -0
  186. truefoundry/ml/constants.py +84 -0
  187. truefoundry/ml/enums.py +70 -0
  188. truefoundry/ml/env_vars.py +13 -0
  189. truefoundry/ml/exceptions.py +8 -0
  190. truefoundry/ml/git_info.py +60 -0
  191. truefoundry/ml/internal_namespace.py +52 -0
  192. truefoundry/ml/log_types/__init__.py +4 -0
  193. truefoundry/ml/log_types/artifacts/artifact.py +427 -0
  194. truefoundry/ml/log_types/artifacts/constants.py +33 -0
  195. truefoundry/ml/log_types/artifacts/dataset.py +383 -0
  196. truefoundry/ml/log_types/artifacts/general_artifact.py +110 -0
  197. truefoundry/ml/log_types/artifacts/model.py +628 -0
  198. truefoundry/ml/log_types/artifacts/model_extras.py +48 -0
  199. truefoundry/ml/log_types/artifacts/utils.py +161 -0
  200. truefoundry/ml/log_types/image/__init__.py +3 -0
  201. truefoundry/ml/log_types/image/constants.py +8 -0
  202. truefoundry/ml/log_types/image/image.py +358 -0
  203. truefoundry/ml/log_types/image/image_normalizer.py +101 -0
  204. truefoundry/ml/log_types/image/types.py +68 -0
  205. truefoundry/ml/log_types/plot.py +281 -0
  206. truefoundry/ml/log_types/pydantic_base.py +10 -0
  207. truefoundry/ml/log_types/utils.py +12 -0
  208. truefoundry/ml/logger.py +17 -0
  209. truefoundry/ml/login.py +241 -0
  210. truefoundry/ml/mlfoundry_api.py +1620 -0
  211. truefoundry/ml/mlfoundry_run.py +1238 -0
  212. truefoundry/ml/run_utils.py +102 -0
  213. truefoundry/ml/services/__init__.py +0 -0
  214. truefoundry/ml/services/auth_service.py +109 -0
  215. truefoundry/ml/services/entities.py +108 -0
  216. truefoundry/ml/services/servicefoundry_service.py +35 -0
  217. truefoundry/ml/services/utils.py +122 -0
  218. truefoundry/ml/session.py +271 -0
  219. truefoundry/ml/validation_utils.py +346 -0
  220. truefoundry/pydantic_v1.py +5 -1
  221. {truefoundry-0.3.2.dist-info → truefoundry-0.4.0.dev0.dist-info}/METADATA +19 -12
  222. truefoundry-0.4.0.dev0.dist-info/RECORD +342 -0
  223. truefoundry-0.3.2.dist-info/RECORD +0 -136
  224. {truefoundry-0.3.2.dist-info → truefoundry-0.4.0.dev0.dist-info}/WHEEL +0 -0
  225. {truefoundry-0.3.2.dist-info → truefoundry-0.4.0.dev0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,161 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import posixpath
5
+ import shutil
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Sequence, Tuple, Union
9
+
10
+ from truefoundry.ml.exceptions import MlFoundryException
11
+ from truefoundry.ml.log_types.artifacts.constants import DESCRIPTION_MAX_LENGTH
12
+
13
+ logger = logging.getLogger("truefoundry.ml")
14
+
15
+
16
+ def _copy_tree(src_path, dest_path, symlinks=False, ignore_dangling_symlinks=False):
17
+ os.makedirs(dest_path, exist_ok=True)
18
+ for item in os.listdir(src_path):
19
+ src = os.path.join(src_path, item)
20
+ dest = os.path.join(dest_path, item)
21
+ if os.path.isdir(src):
22
+ _copy_tree(
23
+ src,
24
+ dest,
25
+ symlinks=symlinks,
26
+ ignore_dangling_symlinks=ignore_dangling_symlinks,
27
+ )
28
+ else:
29
+ shutil.copy2(src, dest, follow_symlinks=True)
30
+
31
+
32
+ def is_destination_path_dirlike(dest_path) -> bool:
33
+ if not dest_path:
34
+ return True
35
+
36
+ if dest_path.endswith(os.sep) or dest_path.endswith(posixpath.sep):
37
+ return True
38
+
39
+ if os.path.exists(dest_path) and os.path.isdir(dest_path):
40
+ return True
41
+
42
+ return False
43
+
44
+
45
+ def _copy_additional_files(
46
+ root_dir: str,
47
+ files_dir: str, # relative to root dir e.g. "files/"
48
+ model_dir: Optional[str], # relative to files_dir e.g "model/"
49
+ additional_files: Sequence[Tuple[Union[str, Path], Optional[str]]],
50
+ ignore_model_dir_dest_conflict: bool = False,
51
+ ):
52
+ """
53
+
54
+ File copying examples:
55
+ # non ambiguous
56
+ # a.txt -> /tmp/ result /tmp/a.txt
57
+ # a.txt -> /tmp/a/ result /tmp/a/a.txt
58
+ # a.txt -> /tmp/a/b/c/d.txt result /tmp/a/b/c/d.txt
59
+ # .gitignore -> /tmp/.gitignore result /tmp/.gitignore
60
+
61
+ # ambiguous but destination directory exists
62
+ # a.txt -> /tmp result /tmp/a.txt
63
+ # a.txt -> /tmp/a (and /tmp/a/ exists) result /tmp/a/a.txt
64
+
65
+ # ambiguous - when the destination can't be reliably distinguished as a directory
66
+ # a -> /tmp/a result /tmp/a
67
+ # a -> /tmp/b result /tmp/b
68
+ # a -> /tmp/a.txt result /tmp/a.txt
69
+ # .gitignore -> /tmp/.gitinclude result /tmp/.gitinclude
70
+ # a.txt -> /tmp/a result /tmp/a
71
+ """
72
+ for src_path, dest_path in additional_files:
73
+ src_path = str(src_path)
74
+ if not os.path.exists(src_path):
75
+ raise MlFoundryException(
76
+ f"Source path {src_path!r} in `additional_files` does not exist."
77
+ )
78
+ dest_path = dest_path or ""
79
+ normalized_path = os.path.normpath(dest_path)
80
+ if dest_path.endswith(os.sep) or dest_path.endswith(posixpath.sep):
81
+ normalized_path += os.sep
82
+ dest_path = normalized_path.lstrip(os.sep)
83
+
84
+ if (
85
+ model_dir
86
+ and dest_path.startswith(model_dir)
87
+ and not ignore_model_dir_dest_conflict
88
+ ):
89
+ logger.warning(
90
+ f"Destination path {dest_path!r} in `additional_files` conflicts with "
91
+ f"reserved path {model_dir!r}/ which is being used to store the model. "
92
+ f"This might cause errors"
93
+ )
94
+
95
+ files_abs_dir = os.path.join(root_dir, files_dir)
96
+ dest_abs_path = os.path.join(files_abs_dir, dest_path)
97
+
98
+ if os.path.isfile(src_path):
99
+ _src = src_path
100
+ if is_destination_path_dirlike(dest_abs_path):
101
+ os.makedirs(dest_abs_path, exist_ok=True)
102
+ _dst = os.path.relpath(
103
+ os.path.join(dest_abs_path, os.path.basename(_src)), files_abs_dir
104
+ )
105
+ else:
106
+ os.makedirs(os.path.dirname(dest_abs_path), exist_ok=True)
107
+ _dst = os.path.relpath(dest_abs_path, files_abs_dir)
108
+ logger.info(f"Adding file {_src} as /{_dst}")
109
+ shutil.copy2(src_path, dest_abs_path, follow_symlinks=True)
110
+ elif os.path.isdir(src_path):
111
+ os.makedirs(dest_abs_path, exist_ok=True)
112
+ _src = src_path.rstrip("/")
113
+ _dst = os.path.relpath(dest_abs_path, files_abs_dir).rstrip("/")
114
+ logger.info(f"Adding contents of {_src}/ to /{_dst}/")
115
+ _copy_tree(
116
+ src_path=src_path,
117
+ dest_path=dest_abs_path,
118
+ symlinks=True,
119
+ ignore_dangling_symlinks=False,
120
+ )
121
+
122
+
123
+ def _validate_description(description: Optional[str]):
124
+ if description is not None:
125
+ if not isinstance(description, str):
126
+ raise MlFoundryException(
127
+ "`description` must be either `None` or type `str`"
128
+ )
129
+ if len(description) > DESCRIPTION_MAX_LENGTH:
130
+ raise MlFoundryException(
131
+ f"`description` cannot be longer than {DESCRIPTION_MAX_LENGTH} characters"
132
+ )
133
+
134
+
135
+ def _validate_artifact_metadata(metadata: Dict[str, Any]):
136
+ if not isinstance(metadata, dict):
137
+ raise MlFoundryException("`metadata` must be json serializable dict")
138
+ try:
139
+ json.dumps(metadata)
140
+ except ValueError as ve:
141
+ raise MlFoundryException("`metadata` must be json serializable dict") from ve
142
+
143
+
144
+ def calculate_local_directory_size(
145
+ directory: tempfile.TemporaryDirectory, # type: ignore[type-arg]
146
+ ):
147
+ """
148
+ Tells about the size of the artifact
149
+
150
+ Args:
151
+ directory (str): directory path
152
+
153
+ Returns:
154
+ total size of the artifact
155
+ """
156
+ total_size = 0
157
+ for path, _dirs, files in os.walk(directory.name):
158
+ for f in files:
159
+ file_path = os.path.join(path, f)
160
+ total_size += os.stat(file_path).st_size
161
+ return total_size
@@ -0,0 +1,3 @@
1
+ from truefoundry.ml.log_types.image.image import Image
2
+
3
+ __all__ = ["Image"]
@@ -0,0 +1,8 @@
1
+ import re
2
+
3
+ IMAGE_METADATA_FILE_NAME = "image_metadata.json"
4
+ DEFAULT_IMAGE_FORMAT = "png"
5
+ MISSING_PILLOW_PACKAGE_MESSAGE = (
6
+ "We need PIL package to save image.\nTo install, run `pip install pillow`"
7
+ )
8
+ IMAGE_KEY_REGEX = re.compile(r"^[a-zA-Z0-9-_]+$")
@@ -0,0 +1,358 @@
1
+ import json
2
+ import os
3
+ import posixpath
4
+ import shutil
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
8
+
9
+ import numpy as np
10
+
11
+ from truefoundry.ml.autogen.client import ( # type: ignore[attr-defined]
12
+ ArtifactType,
13
+ )
14
+ from truefoundry.ml.exceptions import MlFoundryException
15
+ from truefoundry.ml.log_types.artifacts.artifact import (
16
+ ArtifactVersionInternalMetadata,
17
+ _log_artifact_version_helper,
18
+ )
19
+ from truefoundry.ml.log_types.artifacts.constants import (
20
+ FILES_DIR,
21
+ INTERNAL_METADATA_PATH,
22
+ )
23
+ from truefoundry.ml.log_types.image.constants import (
24
+ DEFAULT_IMAGE_FORMAT,
25
+ IMAGE_KEY_REGEX,
26
+ IMAGE_METADATA_FILE_NAME,
27
+ MISSING_PILLOW_PACKAGE_MESSAGE,
28
+ )
29
+ from truefoundry.ml.log_types.image.image_normalizer import normalize_image
30
+ from truefoundry.ml.log_types.image.types import BoundingBoxGroups, ClassGroups
31
+ from truefoundry.ml.log_types.pydantic_base import PydanticBase
32
+ from truefoundry.ml.run_utils import get_module
33
+
34
+ if TYPE_CHECKING:
35
+ # noinspection PyUnresolvedReferences
36
+ import PIL.Image
37
+ from numpy.typing import NDArray
38
+
39
+ from truefoundry.ml.mlfoundry_run import MlFoundryRun
40
+
41
+ ImageLikeNDArray = Union[
42
+ "NDArray[np.int8]",
43
+ "NDArray[np.uint8]",
44
+ "NDArray[np.float32]",
45
+ "NDArray[np.float64]",
46
+ "NDArray[np.bool_]",
47
+ ]
48
+ DataOrPathType = Union[str, Path, ImageLikeNDArray, "PIL.Image.Image"]
49
+ ClassGroupsType = Dict[str, Union[str, List[str]]]
50
+ BBoxGrouptype = Dict[str, List[Dict[str, Any]]]
51
+
52
+
53
+ def validate_key_name(key: str):
54
+ if not key or not IMAGE_KEY_REGEX.match(key):
55
+ raise MlFoundryException(
56
+ f"Invalid run image key: {key} should only contain alphanumeric, hyphen or underscore"
57
+ )
58
+
59
+
60
+ class ImageVersionInternalMetadata(ArtifactVersionInternalMetadata):
61
+ image_file: str
62
+ image_metadata_file: str
63
+
64
+
65
+ class Image:
66
+ def __init__(
67
+ self,
68
+ data_or_path: DataOrPathType,
69
+ caption: Optional[str] = None,
70
+ class_groups: Optional[ClassGroupsType] = None,
71
+ bbox_groups: Optional[BBoxGrouptype] = None,
72
+ ):
73
+ """Represent and log image using this class in `mlfoundry`.
74
+
75
+ You can initialize `truefoundry.ml.Image` by either by using a local path
76
+ or you can use a numpy array / PIL.Image object.
77
+
78
+ If you are using numpy array, we only support the following data types,
79
+ - bool
80
+ - integer [0 - 255]
81
+ - unsigned integer [0 - 255]
82
+ - float [0.0 - 1.0]
83
+
84
+ Any out of range value will be clipped.
85
+
86
+ As for array shape/dim, we follow the following structures,
87
+ - H x W (Grayscale)
88
+ - H x W x 1 (Grayscale)
89
+ - H x W x 3 (an RGB channel order is assumed)
90
+ - H x W x 4 (an RGBA channel order is assumed)
91
+
92
+ `PIL` package is required to log images. To install the `PIL` package,
93
+ run `pip install pillow`.
94
+
95
+ We can also log class names and bounding boxes associated with the image.
96
+ Class names and bounding boxes should be always grouped under `actuals` or
97
+ `predictions`. For example, if we have an image where the ground truth class
98
+ is "cat" and predicted class is "dog", we can represent it like,
99
+
100
+ ```python
101
+ from truefoundry.ml import Image
102
+
103
+ Image(
104
+ data_or_path=imarray,
105
+ class_groups={"actuals": "dog", "predictions": "cat"}
106
+ )
107
+ ```
108
+
109
+ You can define a bounding box using the following dictionary structure,
110
+ ```python
111
+ {
112
+ "position": {"min_x": 15, "min_y": 5, "max_x": 20, "max_y": 30}, # required, defines the position of the bounding box
113
+ # (min_x, min_y) defines the top left and
114
+ # (max_x, max_y) defines the bottom right corner of the box.
115
+ "class_name": "dog", # required, the class name of the bounding box
116
+ "caption": "dog", # optional, the caption of the bounding box.
117
+ # If not passed, the class name is set as caption.
118
+ }
119
+ ```
120
+
121
+ Args:
122
+ data_or_path (Union[str, Path, "numpy.ndarray", "PIL.Image.Image"]):
123
+ Either the local path or the image object (Numpy array or PIL Image).
124
+ caption (Optional[str], optional): A string caption or label for the image.
125
+ class_groups (Optional[Dict[str, Union[str, List[str]]]], optional):
126
+ Class names associated with the image. Expects class name(s) grouped by
127
+ `predictions` or `actuals`.
128
+ bbox_groups (Optional[Dict[str, List[Dict]]], optional): Bounding boxes
129
+ associated with the image. Expects bounding boxes grouped by `predictions`
130
+ or `actuals`.
131
+
132
+ Examples:
133
+ ### Logging images with caption and class names
134
+
135
+ ```python
136
+ from truefoundry.ml import get_client, Image
137
+ import numpy as np
138
+
139
+ client = get_client()
140
+ run = client.create_run(
141
+ ml_repo="my-classification-project",
142
+ )
143
+
144
+ imarray = np.random.randint(low=0, high=256, size=(100, 100, 3))
145
+
146
+ images_to_log = {
147
+ "logged-image-array": Image(
148
+ data_or_path=imarray,
149
+ caption="testing image logging",
150
+ class_groups={"actuals": "dog", "predictions": "cat"},
151
+ ),
152
+ }
153
+ run.log_images(images_to_log, step=1)
154
+
155
+ run.end()
156
+ ```
157
+
158
+ ### Logging images for a multi-label classification problem
159
+
160
+ ```python
161
+ from truefoundry.ml import Image
162
+
163
+ images_to_log = {
164
+ "logged-image-array": Image(
165
+ data_or_path=imarray,
166
+ caption="testing image logging",
167
+ class_groups={"actuals": ["dog", "human"], "predictions": ["cat", "human"]},
168
+ ),
169
+ }
170
+
171
+ run.log_images(images_to_log, step=1)
172
+ ```
173
+
174
+ ### Logging images with bounding boxes
175
+
176
+ ```python
177
+ from truefoundry.ml import Image
178
+
179
+ images_to_log = {
180
+ "logged-image-array": Image(
181
+ data_or_path=imarray,
182
+ caption="testing image logging",
183
+ bbox_groups={
184
+ "predictions": [
185
+ {
186
+ "position": {"min_x": 5, "min_y": 5, "max_x": 20, "max_y": 30},
187
+ "class_name": "cat",
188
+ }
189
+ ],
190
+ "actuals": [
191
+ {
192
+ "position": {"min_x": 15, "min_y": 5, "max_x": 20, "max_y": 30},
193
+ "class_name": "dog",
194
+ "caption": "dog",
195
+ }
196
+ ],
197
+ },
198
+ ),
199
+ }
200
+
201
+ run.log_images(images_to_log, step=1)
202
+ ```
203
+ """
204
+
205
+ self._caption: Optional[str] = None
206
+ self._class_groups: Optional[ClassGroups] = None
207
+ self._bbox_groups: Optional[BoundingBoxGroups] = None
208
+
209
+ self._image: Optional["PIL.Image.Image"] = None
210
+ self._image_artifact_path = None
211
+ self._local_image_path: Optional[str] = None
212
+
213
+ self._init_image(data_or_path)
214
+ self._init_metadata(
215
+ caption=caption, class_groups=class_groups, bbox_groups=bbox_groups
216
+ )
217
+
218
+ @property
219
+ def image(self) -> "PIL.Image.Image":
220
+ if self._image is None:
221
+ raise MlFoundryException("Image is not initialized")
222
+ return self._image
223
+
224
+ @property
225
+ def caption(self) -> Optional[str]:
226
+ return self._caption
227
+
228
+ @property
229
+ def class_groups(self) -> Optional[ClassGroups]:
230
+ return self._class_groups
231
+
232
+ @property
233
+ def bbox_groups(self) -> Optional[BoundingBoxGroups]:
234
+ return self._bbox_groups
235
+
236
+ def save_image_to_local_dir(self, local_dir) -> str:
237
+ if self._local_image_path is None:
238
+ image_file_name = self._serialize_and_save_image_as_artifact(local_dir)
239
+ else:
240
+ image_file_name = self._copy_local_image_as_artifact(local_dir)
241
+
242
+ return image_file_name
243
+
244
+ def _serialize_and_save_image_as_artifact(self, local_dir: str) -> str:
245
+ file_name = f"image.{DEFAULT_IMAGE_FORMAT}"
246
+ local_path = os.path.join(local_dir, file_name)
247
+ self.image.save(local_path)
248
+
249
+ return file_name
250
+
251
+ def _copy_local_image_as_artifact(self, local_dir: str) -> str:
252
+ assert self._local_image_path is not None
253
+ file_name = os.path.basename(self._local_image_path)
254
+ new_local_image_path = os.path.join(local_dir, file_name)
255
+ shutil.copy2(self._local_image_path, new_local_image_path)
256
+ return file_name
257
+
258
+ def _init_image(self, data_or_path: DataOrPathType):
259
+ pil_image_module = get_module(
260
+ module_name="PIL.Image",
261
+ required=True,
262
+ error_message=MISSING_PILLOW_PACKAGE_MESSAGE,
263
+ )
264
+ if isinstance(data_or_path, (str, Path)):
265
+ self._local_image_path = os.path.abspath(data_or_path)
266
+ with pil_image_module.open(data_or_path) as image:
267
+ image.load()
268
+ self._image = image
269
+ else:
270
+ self._image = normalize_image(data_or_path)
271
+
272
+ def _init_metadata(
273
+ self,
274
+ caption: Optional[str],
275
+ class_groups: Optional[ClassGroupsType],
276
+ bbox_groups: Optional[Dict[str, Any]],
277
+ ):
278
+ if caption is not None:
279
+ self._caption = str(caption)
280
+
281
+ if class_groups:
282
+ self._class_groups = ClassGroups.parse_obj(class_groups)
283
+
284
+ if bbox_groups:
285
+ self._bbox_groups = BoundingBoxGroups.parse_obj(bbox_groups)
286
+
287
+ def _to_dict(self) -> Dict[str, Any]:
288
+ dict_ = {}
289
+ dict_["caption"] = self._caption
290
+ dict_["class_groups"] = (
291
+ self.class_groups.to_dict() if self.class_groups is not None else None
292
+ )
293
+ dict_["bbox_groups"] = (
294
+ self.bbox_groups.to_dict() if self.bbox_groups is not None else None
295
+ )
296
+
297
+ return dict_
298
+
299
+ def save(
300
+ self,
301
+ run: "MlFoundryRun",
302
+ key: str,
303
+ step: int = 0,
304
+ ):
305
+ validate_key_name(key)
306
+
307
+ # creating temp dir, files directory, .truefoundry directory
308
+ temp_dir = tempfile.TemporaryDirectory(prefix="truefoundry-")
309
+
310
+ local_internal_metadata_path = os.path.join(
311
+ temp_dir.name, INTERNAL_METADATA_PATH
312
+ )
313
+ os.makedirs(os.path.dirname(local_internal_metadata_path), exist_ok=True)
314
+
315
+ local_files_dir = os.path.join(temp_dir.name, FILES_DIR)
316
+ os.makedirs(local_files_dir, exist_ok=True)
317
+
318
+ try:
319
+ # saving the image file
320
+ image_file_name = self.save_image_to_local_dir(local_files_dir)
321
+
322
+ # saving the image_metadata
323
+ local_image_metadata_file_path = os.path.join(
324
+ local_files_dir, IMAGE_METADATA_FILE_NAME
325
+ )
326
+ with open(local_image_metadata_file_path, "w") as fp:
327
+ fp.write(ImageRunLogType(value=self._to_dict()).json())
328
+
329
+ # creating and saving the internal_metadata file
330
+ internal_metadata = ImageVersionInternalMetadata(
331
+ files_dir=FILES_DIR,
332
+ # joining with posixpath for avoiding backslash in case logged from windows machine
333
+ image_file=posixpath.join(FILES_DIR, image_file_name),
334
+ image_metadata_file=posixpath.join(FILES_DIR, IMAGE_METADATA_FILE_NAME),
335
+ )
336
+ with open(local_internal_metadata_path, "w") as f:
337
+ json.dump(internal_metadata.dict(), f)
338
+
339
+ except Exception as e:
340
+ temp_dir.cleanup()
341
+ raise MlFoundryException("Failed to log Image") from e
342
+
343
+ _log_artifact_version_helper(
344
+ run=run,
345
+ name=key,
346
+ artifact_type=ArtifactType.IMAGE,
347
+ artifact_dir=temp_dir,
348
+ internal_metadata=internal_metadata,
349
+ step=step,
350
+ )
351
+
352
+
353
+ class ImageRunLogType(PydanticBase):
354
+ value: Dict[str, Any]
355
+
356
+ @staticmethod
357
+ def get_log_type() -> str:
358
+ return "image"
@@ -0,0 +1,101 @@
1
+ # The logic is copied from
2
+ # https://github.com/truefoundry/mlfoundry-server/blob/c2334ace8f35322cd5e50c275b8df08327688c01/mlflow/tracking/client.py#L1188
3
+ from typing import TYPE_CHECKING, Union
4
+
5
+ import numpy as np
6
+
7
+ from truefoundry.ml.exceptions import MlFoundryException
8
+ from truefoundry.ml.log_types.image.constants import MISSING_PILLOW_PACKAGE_MESSAGE
9
+ from truefoundry.ml.logger import logger
10
+ from truefoundry.ml.run_utils import get_module
11
+
12
+ if TYPE_CHECKING:
13
+ import PIL.Image
14
+ from numpy.typing import NDArray
15
+
16
+ ImageLikeNDArray = Union[
17
+ "NDArray[np.int8]",
18
+ "NDArray[np.uint8]",
19
+ "NDArray[np.float32]",
20
+ "NDArray[np.float64]",
21
+ "NDArray[np.bool_]",
22
+ ]
23
+
24
+
25
+ def _normalize_to_uint8(x):
26
+ # Based on: https://github.com/matplotlib/matplotlib/blob/06567e021f21be046b6d6dcf00380c1cb9adaf3c/lib/matplotlib/image.py#L684
27
+
28
+ is_int = np.issubdtype(x.dtype, np.integer)
29
+ low = 0
30
+ high = 255 if is_int else 1
31
+ if x.min() < low or x.max() > high:
32
+ msg = (
33
+ "Out-of-range values are detected. "
34
+ "Clipping array (dtype: '{}') to [{}, {}]".format(x.dtype, low, high)
35
+ )
36
+ logger.warning(msg)
37
+ x = np.clip(x, low, high)
38
+
39
+ # float or bool
40
+ if not is_int:
41
+ x = x * 255
42
+
43
+ return x.astype(np.uint8)
44
+
45
+
46
+ def _convert_numpy_to_pil_image(image: ImageLikeNDArray) -> "PIL.Image.Image":
47
+ pil_image_module = get_module(
48
+ module_name="PIL.Image",
49
+ required=True,
50
+ error_message=MISSING_PILLOW_PACKAGE_MESSAGE,
51
+ )
52
+ valid_data_types = {
53
+ "b": "bool",
54
+ "i": "signed integer",
55
+ "u": "unsigned integer",
56
+ "f": "floating",
57
+ }
58
+
59
+ if image.dtype.kind not in valid_data_types.keys():
60
+ raise TypeError(
61
+ "Invalid array data type: '{}'. Must be one of {}".format(
62
+ image.dtype, list(valid_data_types.values())
63
+ )
64
+ )
65
+
66
+ if image.ndim not in [2, 3]:
67
+ raise ValueError(
68
+ "`image` must be a 2D or 3D array but got a {}D array".format(image.ndim)
69
+ )
70
+
71
+ if (image.ndim == 3) and (image.shape[2] not in [1, 3, 4]):
72
+ raise ValueError(
73
+ "Invalid channel length: {}. Must be one of [1, 3, 4]".format(
74
+ image.shape[2]
75
+ )
76
+ )
77
+
78
+ # squeeze a 3D grayscale image since `Image.fromarray` doesn't accept it.
79
+ if image.ndim == 3 and image.shape[2] == 1:
80
+ image = image[:, :, 0]
81
+
82
+ image = _normalize_to_uint8(image)
83
+
84
+ return pil_image_module.fromarray(image)
85
+
86
+
87
+ def normalize_image(
88
+ image: Union["PIL.Image.Image", ImageLikeNDArray],
89
+ ) -> "PIL.Image.Image":
90
+ pil_image_module = get_module(
91
+ module_name="PIL.Image",
92
+ required=True,
93
+ error_message=MISSING_PILLOW_PACKAGE_MESSAGE,
94
+ )
95
+ if isinstance(image, pil_image_module.Image):
96
+ return image
97
+ if isinstance(image, np.ndarray):
98
+ return _convert_numpy_to_pil_image(image)
99
+ raise MlFoundryException(
100
+ f"image should be of type PIL Image/np.ndarray got type {type(image)}"
101
+ )
@@ -0,0 +1,68 @@
1
+ import enum
2
+ from typing import Dict, List, Optional
3
+
4
+ from truefoundry.pydantic_v1 import BaseModel, NonEmptyStr, root_validator
5
+
6
+
7
+ class Group(enum.Enum):
8
+ ACTUALS = "actuals"
9
+ PREDICTIONS = "predictions"
10
+
11
+
12
+ class Position(BaseModel):
13
+ min_x: float
14
+ min_y: float
15
+ max_x: float
16
+ max_y: float
17
+
18
+ class Config:
19
+ allow_mutation = False
20
+
21
+
22
+ class BoundingBox(BaseModel):
23
+ position: Position
24
+ class_name: NonEmptyStr
25
+ caption: Optional[str] = None
26
+
27
+ class Config:
28
+ allow_mutation = False
29
+
30
+ @root_validator(pre=True)
31
+ def set_caption_if_not_passed(cls, values):
32
+ if not values.get("caption"):
33
+ values["caption"] = values.get("class_name")
34
+
35
+ return values
36
+
37
+
38
+ class BoundingBoxGroups(BaseModel):
39
+ __root__: Dict[Group, List[BoundingBox]]
40
+
41
+ class Config:
42
+ allow_mutation = False
43
+ use_enum_values = True
44
+
45
+ def to_dict(self):
46
+ return self.dict()["__root__"]
47
+
48
+
49
+ class ClassGroups(BaseModel):
50
+ __root__: Dict[Group, List[NonEmptyStr]]
51
+
52
+ class Config:
53
+ allow_mutation = False
54
+ use_enum_values = True
55
+
56
+ @root_validator(pre=True)
57
+ def transform_class_to_classes(cls, values):
58
+ values = values["__root__"]
59
+ processed_values = {}
60
+ for group, classes in values.items():
61
+ if isinstance(classes, str):
62
+ processed_values[group] = [classes]
63
+ else:
64
+ processed_values[group] = classes
65
+ return {"__root__": processed_values}
66
+
67
+ def to_dict(self):
68
+ return self.dict()["__root__"]