datatailr 0.1.0__py3-none-any.whl → 0.1.2__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 datatailr might be problematic. Click here for more details.

datatailr/__init__.py CHANGED
@@ -0,0 +1,63 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ from datatailr.wrapper import (
12
+ dt__Blob,
13
+ dt__Dns,
14
+ dt__Email,
15
+ dt__Group,
16
+ dt__Job,
17
+ dt__Kv,
18
+ dt__Log,
19
+ dt__Node,
20
+ dt__Registry,
21
+ dt__Service,
22
+ dt__Settings,
23
+ dt__Sms,
24
+ dt__System,
25
+ dt__Tag,
26
+ dt__User,
27
+ )
28
+ from datatailr.group import Group
29
+ from datatailr.user import User
30
+ from datatailr.acl import ACL
31
+ from datatailr.blob import Blob
32
+ from datatailr.build import Image
33
+ from datatailr.dt_json import dt_json
34
+ from datatailr.utils import Environment, is_dt_installed
35
+ from datatailr.version import __version__
36
+
37
+
38
+ __all__ = [
39
+ "ACL",
40
+ "Blob",
41
+ "Environment",
42
+ "Group",
43
+ "Image",
44
+ "User",
45
+ "__version__",
46
+ "dt__Blob",
47
+ "dt__Dns",
48
+ "dt__Email",
49
+ "dt__Group",
50
+ "dt__Job",
51
+ "dt__Kv",
52
+ "dt__Log",
53
+ "dt__Node",
54
+ "dt__Registry",
55
+ "dt__Service",
56
+ "dt__Settings",
57
+ "dt__Sms",
58
+ "dt__System",
59
+ "dt__Tag",
60
+ "dt__User",
61
+ "dt_json",
62
+ "is_dt_installed",
63
+ ]
datatailr/acl.py ADDED
@@ -0,0 +1,80 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ import json
12
+ from typing import List, Optional, Union
13
+
14
+ from datatailr import Group, User
15
+
16
+
17
+ class ACL:
18
+ """
19
+ A class to represent an Access Control List (ACL) for managing permissions.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ user: Union[User, str],
25
+ group: Optional[Union[Group, str]] = None,
26
+ permissions: Optional[List[str]] = None,
27
+ ):
28
+ if user is None:
29
+ user = User.signed_user()
30
+ self.user = user if isinstance(user, User) else User.get(user)
31
+ if self.user is not None:
32
+ self.group = (
33
+ group if group and isinstance(group, Group) else self.user.primary_group
34
+ )
35
+ self.permissions = permissions or "rwr---"
36
+
37
+ self.__group_can_read = False
38
+ self.__group_can_write = False
39
+ self.__user_can_read = False
40
+ self.__user_can_write = False
41
+ self.__world_readable = False
42
+
43
+ self.__parse_permissions()
44
+
45
+ def __parse_permissions(self):
46
+ """
47
+ Parse the permissions and set the corresponding flags.
48
+ """
49
+ if isinstance(self.permissions, str):
50
+ self.permissions = list(self.permissions)
51
+ if len(self.permissions) != 6:
52
+ raise ValueError(
53
+ "Permissions must be a list of 6 characters. representing 'r', 'w', and '-' for user, group, and world."
54
+ )
55
+ self.__user_can_read = self.permissions[0] == "r"
56
+ self.__user_can_write = self.permissions[1] == "w"
57
+ self.__group_can_read = self.permissions[2] == "r"
58
+ self.__group_can_write = self.permissions[3] == "w"
59
+ self.__world_readable = self.permissions[4] == "r"
60
+ self.__world_writable = self.permissions[5] == "w"
61
+
62
+ def __repr__(self):
63
+ return (
64
+ f"ACL(user={self.user}, group={self.group}, permissions={self.permissions})"
65
+ )
66
+
67
+ def to_dict(self):
68
+ return {
69
+ "user": self.user.name,
70
+ "group": self.group.name if self.group else self.user.primary_group.name,
71
+ "group_can_read": self.__group_can_read,
72
+ "group_can_write": self.__group_can_write,
73
+ "user_can_read": self.__user_can_read,
74
+ "user_can_write": self.__user_can_write,
75
+ "world_readable": self.__world_readable,
76
+ "world_writable": self.__world_writable,
77
+ }
78
+
79
+ def to_json(self):
80
+ return json.dumps(self.to_dict())
datatailr/blob.py ADDED
@@ -0,0 +1,103 @@
1
+ ##########################################################################
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ ##########################################################################
10
+
11
+ from __future__ import annotations
12
+
13
+ import tempfile
14
+
15
+ from datatailr import dt__Blob
16
+
17
+
18
+ class Blob:
19
+ __blob_storage__ = dt__Blob()
20
+
21
+ def ls(self, path: str) -> list[str]:
22
+ """
23
+ List files in the specified path.
24
+
25
+ :param path: The path to list files from.
26
+ :return: A list of file names in the specified path.
27
+ """
28
+ return self.__blob_storage__.ls(path)
29
+
30
+ def get_file(self, name: str, path: str):
31
+ """
32
+ Copy a blob file to a local file.
33
+
34
+ Args:
35
+ name (str): The name of the blob to retrieve.
36
+ path (str): The path to store the blob as a file.
37
+ """
38
+ return self.__blob_storage__.cp(f"blob:///{name}", path)
39
+
40
+ def put_file(self, name: str, path: str):
41
+ """
42
+ Copy a local file to a blob.
43
+
44
+ Args:
45
+ name (str): The name of the blob to create.
46
+ path (str): The path of the local file to copy.
47
+ """
48
+ return self.__blob_storage__.cp(path, f"blob:///{name}")
49
+
50
+ def exists(self, name: str) -> bool:
51
+ """
52
+ Check if a blob exists.
53
+
54
+ Args:
55
+ name (str): The name of the blob to check.
56
+
57
+ Returns:
58
+ bool: True if the blob exists, False otherwise.
59
+ """
60
+ return self.__blob_storage__.exists(name)
61
+
62
+ def delete(self, name: str):
63
+ """
64
+ Delete a blob.
65
+
66
+ Args:
67
+ name (str): The name of the blob to delete.
68
+ """
69
+ return self.__blob_storage__.rm(name)
70
+
71
+ def get_blob(self, name: str):
72
+ """
73
+ Get a blob object.
74
+
75
+ Args:
76
+ name (str): The name of the blob to retrieve.
77
+
78
+ Returns:
79
+ Blob: The blob object.
80
+ """
81
+ # Since direct reading and writting of blobs is not implemented yet, we are using a temporary file.
82
+ # This is a workaround to allow reading the blob content directly from the blob storage.
83
+
84
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
85
+ self.get_file(name, temp_file.name)
86
+ with open(temp_file.name, "rb") as f:
87
+ return f.read()
88
+
89
+ def put_blob(self, name: str, blob):
90
+ """
91
+ Put a blob object into the blob storage.
92
+
93
+ Args:
94
+ name (str): The name of the blob to create.
95
+ blob: The blob object to store.
96
+ """
97
+ # Since direct reading and writting of blobs is not implemented yet, we are using a temporary file.
98
+ # This is a workaround to allow writing the blob content directly to the blob storage.
99
+
100
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
101
+ with open(temp_file.name, "wb") as f:
102
+ f.write(blob)
103
+ self.put_file(name, temp_file.name)
@@ -0,0 +1,11 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ from datatailr.build.image import Image as Image
@@ -0,0 +1,87 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ import json
12
+ import os
13
+ from typing import Optional
14
+
15
+ from datatailr import ACL, User
16
+
17
+
18
+ class Image:
19
+ """
20
+ Represents a container image for a job.
21
+ The image is defined based on its' python dependencies and two 'build scripts' expressed as Dockerfile commands.
22
+ All attributes can be initialized with either a string or a file name.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ acl: Optional[ACL] = None,
28
+ python_requirements: str = "",
29
+ build_script_pre: str = "",
30
+ build_script_post: str = "",
31
+ ):
32
+ if acl is None:
33
+ signed_user = User.signed_user()
34
+ if signed_user is None:
35
+ raise ValueError(
36
+ "ACL cannot be None. Please provide a valid ACL or ensure a user is signed in."
37
+ )
38
+ elif not isinstance(acl, ACL):
39
+ raise TypeError("acl must be an instance of ACL.")
40
+ self.acl = acl or ACL(signed_user)
41
+
42
+ if os.path.isfile(python_requirements):
43
+ with open(python_requirements, "r") as f:
44
+ python_requirements = f.read()
45
+ if not isinstance(python_requirements, str):
46
+ raise TypeError(
47
+ "python_requirements must be a string or a file path to a requirements file."
48
+ )
49
+ self.python_requirements = python_requirements
50
+
51
+ if os.path.isfile(build_script_pre):
52
+ with open(build_script_pre, "r") as f:
53
+ build_script_pre = f.read()
54
+ if not isinstance(build_script_pre, str):
55
+ raise TypeError(
56
+ "build_script_pre must be a string or a file path to a script file."
57
+ )
58
+ self.build_script_pre = build_script_pre
59
+
60
+ if os.path.isfile(build_script_post):
61
+ with open(build_script_post, "r") as f:
62
+ build_script_post = f.read()
63
+ if not isinstance(build_script_post, str):
64
+ raise TypeError(
65
+ "build_script_post must be a string or a file path to a script file."
66
+ )
67
+ self.build_script_post = build_script_post
68
+
69
+ def __repr__(self):
70
+ return f"Image(acl={self.acl},)"
71
+
72
+ def to_dict(self):
73
+ """
74
+ Convert the Image instance to a dictionary representation.
75
+ """
76
+ return {
77
+ "acl": self.acl.to_dict(),
78
+ "python_requirements": self.python_requirements,
79
+ "build_script_pre": self.build_script_pre,
80
+ "build_script_post": self.build_script_post,
81
+ }
82
+
83
+ def to_json(self):
84
+ """
85
+ Convert the Image instance to a JSON string representation.
86
+ """
87
+ return json.dumps(self.to_dict(), indent=4)
datatailr/dt_json.py ADDED
@@ -0,0 +1,42 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ import importlib
12
+ import json
13
+
14
+
15
+ class dt_json:
16
+ @staticmethod
17
+ def dumps(obj):
18
+ d = obj.__dict__ if hasattr(obj, "__dict__") else obj
19
+ d["__class__"] = obj.__class__.__name__
20
+ return json.dumps(d)
21
+
22
+ @classmethod
23
+ def loads(cls, json_str):
24
+ d = json.loads(json_str)
25
+ class_name = d.get("__class__")
26
+ module = importlib.import_module("datatailr")
27
+ if class_name and hasattr(module, class_name):
28
+ target_class = getattr(module, class_name)
29
+ obj = target_class.__new__(target_class)
30
+ if hasattr(obj, "from_json"):
31
+ obj.from_json(d)
32
+ return obj
33
+ # fallback: set attributes directly
34
+ for k, v in d.items():
35
+ if k != "__class__":
36
+ setattr(obj, k, v)
37
+ return obj
38
+ return d # fallback to dict if not registered
39
+
40
+ @classmethod
41
+ def load(cls, json_file):
42
+ return json.load(json_file)
datatailr/errors.py ADDED
@@ -0,0 +1,10 @@
1
+ class DatatailrError(Exception):
2
+ """Base class for all DataTailr exceptions."""
3
+
4
+ pass
5
+
6
+
7
+ class BatchJobError(DatatailrError):
8
+ """Exception raised for errors related to batch jobs."""
9
+
10
+ pass
datatailr/group.py ADDED
@@ -0,0 +1,136 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ from typing import Optional, Union
12
+
13
+ from datatailr import dt__Group
14
+
15
+
16
+ class Group:
17
+ """Representing a Datatailr Group
18
+ This class provides methods to interact with the Datatailr Group API.
19
+ It allows you to create, update, delete, and manage groups within the Datatailr platform.
20
+ Attributes:
21
+ name (str): The name of the group.
22
+ members (list): A list of members in the group.
23
+ group_id (int): The unique identifier for the group.
24
+ Static Methods:
25
+ add(name: str) -> 'Group':
26
+ Create a new group with the specified name.
27
+ get(name: str) -> 'Group':
28
+ Retrieve a group by its name.
29
+ list() -> list:
30
+ List all groups available in the Datatailr platform.
31
+ remove(name: str) -> None:
32
+ Remove a group by its name.
33
+ exists(name: str) -> bool:
34
+ Check if a group exists by its name.
35
+ Instance Methods:
36
+ add_users(usernames: list) -> None:
37
+ Add users to the group.
38
+ remove_users(usernames: list) -> None:
39
+ Remove users from the group.
40
+ """
41
+
42
+ # Datatailr Group API Client
43
+ __client__ = dt__Group()
44
+
45
+ def __init__(self, name: str):
46
+ self.__name: str = name
47
+ self.__members: list = []
48
+ self.__group_id: int | None = None
49
+
50
+ self.__refresh__()
51
+
52
+ def __repr__(self):
53
+ return (
54
+ f"Group(name={self.name}, members={self.members}, group_id={self.group_id})"
55
+ )
56
+
57
+ def __str__(self):
58
+ return f"<Group: {self.name} | {self.group_id}>"
59
+
60
+ def __eq__(self, other):
61
+ if not isinstance(other, Group):
62
+ return NotImplemented
63
+ return (
64
+ self.group_id == other.group_id
65
+ and self.name == other.name
66
+ and set(self.members) == set(other.members)
67
+ )
68
+
69
+ def __refresh__(self):
70
+ if not self.name:
71
+ raise ValueError("Name is not set. Cannot refresh group.")
72
+ group = Group.__client__.get(self.name)
73
+ if group:
74
+ self.__name = group["name"]
75
+ self.__members = group["members"]
76
+ self.__group_id = group["group_id"]
77
+ else:
78
+ raise ValueError(f"Group '{self.name}' does not exist.")
79
+
80
+ @property
81
+ def name(self) -> str:
82
+ return self.__name
83
+
84
+ @property
85
+ def members(self) -> list:
86
+ return self.__members
87
+
88
+ @property
89
+ def group_id(self) -> Optional[int]:
90
+ return self.__group_id
91
+
92
+ @staticmethod
93
+ def get(name_or_id: Union[str, int]) -> Optional["Group"]:
94
+ if isinstance(name_or_id, int):
95
+ return next((g for g in Group.ls() if g.group_id == name_or_id), None)
96
+ return Group(name_or_id)
97
+
98
+ @staticmethod
99
+ def add(name: str) -> Optional["Group"]:
100
+ Group.__client__.add(name)
101
+ return Group.get(name)
102
+
103
+ @staticmethod
104
+ def ls() -> list:
105
+ groups = Group.__client__.ls()
106
+ return [Group.get(group["name"]) for group in groups]
107
+
108
+ @staticmethod
109
+ def remove(name: str) -> None:
110
+ Group.__client__.rm(name)
111
+ return None
112
+
113
+ @staticmethod
114
+ def exists(name: str) -> bool:
115
+ return Group.__client__.exists(name)
116
+
117
+ def add_users(self, usernames: list) -> None:
118
+ if not self.name:
119
+ raise ValueError("Name is not set. Cannot add users.")
120
+ Group.__client__.add_users(self.name, ",".join(usernames))
121
+ self.__refresh__()
122
+
123
+ def remove_users(self, usernames: list) -> None:
124
+ if not self.name:
125
+ raise ValueError("Name is not set. Cannot remove users.")
126
+ Group.__client__.rm_users(self.name, ",".join(usernames))
127
+ self.__refresh__()
128
+
129
+ def is_member(self, user) -> bool:
130
+ if not self.name:
131
+ raise ValueError("Name is not set. Cannot check membership.")
132
+ return (
133
+ user.user_id in self.members
134
+ if hasattr(user, "user_id")
135
+ else user in self.members
136
+ )
datatailr/logging.py ADDED
@@ -0,0 +1,93 @@
1
+ # *************************************************************************
2
+ #
3
+ # Copyright (c) 2025 - Datatailr Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This file is part of Datatailr and subject to the terms and conditions
7
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
8
+ # of this file, in parts or full, via any medium is strictly prohibited.
9
+ # *************************************************************************
10
+
11
+ import logging
12
+ import os
13
+ from logging import StreamHandler
14
+ from logging.handlers import RotatingFileHandler
15
+ from typing import Optional
16
+
17
+
18
+ def get_log_level() -> int:
19
+ log_level = os.getenv("DATATAILR_LOG_LEVEL", "INFO").upper()
20
+ if log_level in ["TRACE", "DEBUG"]:
21
+ return logging.DEBUG
22
+ elif log_level == "INFO":
23
+ return logging.INFO
24
+ elif log_level == "WARNING":
25
+ return logging.WARNING
26
+ elif log_level == "ERROR":
27
+ return logging.ERROR
28
+ elif log_level == "CRITICAL":
29
+ return logging.CRITICAL
30
+ else:
31
+ return logging.INFO
32
+
33
+
34
+ class DatatailrLogger:
35
+ def __init__(
36
+ self,
37
+ name: str,
38
+ log_file: Optional[str] = None,
39
+ log_level: int = get_log_level(),
40
+ ):
41
+ """
42
+ Initialize the DatatailrLogger.
43
+
44
+ :param name: Name of the logger.
45
+ :param log_file: Optional file to log messages to.
46
+ :param log_level: Logging level (default: logging.INFO).
47
+ """
48
+ self.logger = logging.getLogger(name)
49
+ self.logger.setLevel(log_level)
50
+
51
+ # Stream handler for stdout/stderr
52
+ stream_handler = StreamHandler()
53
+ stream_handler.setLevel(log_level)
54
+ stream_formatter = logging.Formatter(
55
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
56
+ )
57
+ stream_handler.setFormatter(stream_formatter)
58
+ self.logger.addHandler(stream_handler)
59
+
60
+ # Optional file handler
61
+ if log_file:
62
+ file_handler = RotatingFileHandler(
63
+ log_file, maxBytes=10 * 1024 * 1024, backupCount=5
64
+ )
65
+ file_handler.setLevel(log_level)
66
+ file_formatter = logging.Formatter(
67
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
68
+ )
69
+ file_handler.setFormatter(file_formatter)
70
+ self.logger.addHandler(file_handler)
71
+ self.enable_opentelemetry()
72
+
73
+ def get_logger(self):
74
+ """
75
+ Get the configured logger instance.
76
+
77
+ :return: Configured logger.
78
+ """
79
+ return self.logger
80
+
81
+ def enable_opentelemetry(self):
82
+ """
83
+ Enable OpenTelemetry integration if available.
84
+ """
85
+ try:
86
+ from opentelemetry.instrumentation.logging import LoggingInstrumentor # type: ignore
87
+
88
+ LoggingInstrumentor().instrument(set_logging_format=True)
89
+ self.logger.debug("OpenTelemetry logging instrumentation enabled.")
90
+ except ImportError:
91
+ self.logger.debug(
92
+ "OpenTelemetry is not installed. Proceeding without instrumentation."
93
+ )
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # *************************************************************************
4
+ #
5
+ # Copyright (c) 2025 - Datatailr Inc.
6
+ # All Rights Reserved.
7
+ #
8
+ # This file is part of Datatailr and subject to the terms and conditions
9
+ # defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
10
+ # of this file, in parts or full, via any medium is strictly prohibited.
11
+ # *************************************************************************
12
+
13
+ import importlib
14
+ import os
15
+ import pickle
16
+
17
+ from datatailr import dt__Blob
18
+ from datatailr.logging import DatatailrLogger
19
+
20
+ logger = DatatailrLogger(os.path.abspath(__file__)).get_logger()
21
+
22
+
23
+ def main():
24
+ entry_point = os.environ.get("DATATAILR_BATCH_ENTRYPOINT")
25
+ batch_run_id = os.environ.get("DATATAILR_BATCH_RUN_ID")
26
+ batch_id = os.environ.get("DATATAILR_BATCH_ID")
27
+ job_id = os.environ.get("DATATAILR_JOB_ID")
28
+
29
+ if entry_point is None:
30
+ raise ValueError(
31
+ "Environment variable 'DATATAILR_BATCH_ENTRYPOINT' is not set."
32
+ )
33
+ if batch_run_id is None:
34
+ raise ValueError("Environment variable 'DATATAILR_BATCH_RUN_ID' is not set.")
35
+ if batch_id is None:
36
+ raise ValueError("Environment variable 'DATATAILR_BATCH_ID' is not set.")
37
+ if job_id is None:
38
+ raise ValueError("Environment variable 'DATATAILR_JOB_ID' is not set.")
39
+
40
+ module_name, func_name = entry_point.split(":", 1)
41
+ module = importlib.import_module(module_name)
42
+ function = getattr(module, func_name)
43
+ if not callable(function):
44
+ raise ValueError(
45
+ f"The function '{func_name}' in module '{module_name}' is not callable."
46
+ )
47
+ result = function()
48
+ result_path = f"batch-results-{batch_run_id}-{job_id}.pkl"
49
+ with open(result_path, "wb") as f:
50
+ pickle.dump(result, f)
51
+ blob = dt__Blob()
52
+ blob.cp(result_path, "blob://")
53
+ logger.info(f"{result_path} copied to blob storage.")
54
+
55
+
56
+ if __name__ == "__main__":
57
+ try:
58
+ logger.debug("Starting job execution...")
59
+ main()
60
+ logger.debug("Job executed successfully.")
61
+ except Exception as e:
62
+ logger.error(f"Error during job execution: {e}")
63
+ raise