datatailr 0.1.6__py3-none-any.whl → 0.1.10__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 +1 -35
- datatailr/acl.py +35 -3
- datatailr/blob.py +13 -13
- datatailr/build/image.py +38 -2
- datatailr/dt_json.py +32 -0
- datatailr/errors.py +17 -0
- datatailr/group.py +18 -14
- datatailr/logging.py +21 -10
- datatailr/sbin/datatailr_run.py +147 -0
- datatailr/sbin/datatailr_run_app.py +37 -0
- datatailr/sbin/{run_job.py → datatailr_run_batch.py} +5 -20
- datatailr/sbin/datatailr_run_excel.py +34 -0
- datatailr/sbin/datatailr_run_service.py +34 -0
- datatailr/scheduler/__init__.py +24 -8
- datatailr/scheduler/arguments_cache.py +71 -43
- datatailr/scheduler/base.py +195 -79
- datatailr/scheduler/batch.py +141 -19
- datatailr/scheduler/batch_decorator.py +53 -24
- datatailr/scheduler/constants.py +1 -1
- datatailr/scheduler/schedule.py +117 -0
- datatailr/scheduler/utils.py +3 -1
- datatailr/user.py +21 -21
- datatailr/utils.py +20 -0
- datatailr/wrapper.py +0 -6
- {datatailr-0.1.6.dist-info → datatailr-0.1.10.dist-info}/METADATA +37 -4
- datatailr-0.1.10.dist-info/RECORD +32 -0
- datatailr-0.1.10.dist-info/entry_points.txt +6 -0
- datatailr-0.1.10.dist-info/top_level.txt +1 -0
- datatailr-0.1.6.dist-info/RECORD +0 -29
- datatailr-0.1.6.dist-info/entry_points.txt +0 -2
- datatailr-0.1.6.dist-info/top_level.txt +0 -2
- test_module/__init__.py +0 -17
- test_module/test_submodule.py +0 -38
- {datatailr-0.1.6.dist-info → datatailr-0.1.10.dist-info}/WHEEL +0 -0
- {datatailr-0.1.6.dist-info → datatailr-0.1.10.dist-info}/licenses/LICENSE +0 -0
datatailr/__init__.py
CHANGED
|
@@ -8,30 +8,12 @@
|
|
|
8
8
|
# of this file, in parts or full, via any medium is strictly prohibited.
|
|
9
9
|
# *************************************************************************
|
|
10
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
|
-
mock_cli_tool,
|
|
28
|
-
)
|
|
11
|
+
from datatailr.wrapper import dt__System, mock_cli_tool
|
|
29
12
|
from datatailr.group import Group
|
|
30
13
|
from datatailr.user import User
|
|
31
14
|
from datatailr.acl import ACL
|
|
32
15
|
from datatailr.blob import Blob
|
|
33
16
|
from datatailr.build import Image
|
|
34
|
-
from datatailr.dt_json import dt_json
|
|
35
17
|
from datatailr.utils import Environment, is_dt_installed
|
|
36
18
|
from datatailr.version import __version__
|
|
37
19
|
|
|
@@ -50,21 +32,5 @@ __all__ = [
|
|
|
50
32
|
"User",
|
|
51
33
|
"__version__",
|
|
52
34
|
"__provider__",
|
|
53
|
-
"dt__Blob",
|
|
54
|
-
"dt__Dns",
|
|
55
|
-
"dt__Email",
|
|
56
|
-
"dt__Group",
|
|
57
|
-
"dt__Job",
|
|
58
|
-
"dt__Kv",
|
|
59
|
-
"dt__Log",
|
|
60
|
-
"dt__Node",
|
|
61
|
-
"dt__Registry",
|
|
62
|
-
"dt__Service",
|
|
63
|
-
"dt__Settings",
|
|
64
|
-
"dt__Sms",
|
|
65
|
-
"dt__System",
|
|
66
|
-
"dt__Tag",
|
|
67
|
-
"dt__User",
|
|
68
|
-
"dt_json",
|
|
69
35
|
"is_dt_installed",
|
|
70
36
|
]
|
datatailr/acl.py
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
# of this file, in parts or full, via any medium is strictly prohibited.
|
|
9
9
|
# *************************************************************************
|
|
10
10
|
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
11
13
|
import json
|
|
12
14
|
from typing import List, Optional, Union
|
|
13
15
|
|
|
@@ -40,9 +42,9 @@ class ACL:
|
|
|
40
42
|
self.__user_can_write = False
|
|
41
43
|
self.__world_readable = False
|
|
42
44
|
|
|
43
|
-
self.
|
|
45
|
+
self.__parse_permissions_string()
|
|
44
46
|
|
|
45
|
-
def
|
|
47
|
+
def __parse_permissions_string(self):
|
|
46
48
|
"""
|
|
47
49
|
Parse the permissions and set the corresponding flags.
|
|
48
50
|
"""
|
|
@@ -59,6 +61,19 @@ class ACL:
|
|
|
59
61
|
self.__world_readable = self.permissions[4] == "r"
|
|
60
62
|
self.__world_writable = self.permissions[5] == "w"
|
|
61
63
|
|
|
64
|
+
def _set_permissions_string(self):
|
|
65
|
+
"""
|
|
66
|
+
Set the permissions string based on the current flags.
|
|
67
|
+
"""
|
|
68
|
+
self.permissions = (
|
|
69
|
+
f"{'r' if self.__user_can_read else '-'}"
|
|
70
|
+
f"{'w' if self.__user_can_write else '-'}"
|
|
71
|
+
f"{'r' if self.__group_can_read else '-'}"
|
|
72
|
+
f"{'w' if self.__group_can_write else '-'}"
|
|
73
|
+
f"{'r' if self.__world_readable else '-'}"
|
|
74
|
+
f"{'w' if self.__world_writable else '-'}"
|
|
75
|
+
)
|
|
76
|
+
|
|
62
77
|
def __repr__(self):
|
|
63
78
|
return (
|
|
64
79
|
f"ACL(user={self.user}, group={self.group}, permissions={self.permissions})"
|
|
@@ -67,7 +82,7 @@ class ACL:
|
|
|
67
82
|
def to_dict(self):
|
|
68
83
|
return {
|
|
69
84
|
"user": self.user.name,
|
|
70
|
-
"group": self.group.name if self.group else
|
|
85
|
+
"group": self.group.name if self.group else "dtusers",
|
|
71
86
|
"group_can_read": self.__group_can_read,
|
|
72
87
|
"group_can_write": self.__group_can_write,
|
|
73
88
|
"user_can_read": self.__user_can_read,
|
|
@@ -76,5 +91,22 @@ class ACL:
|
|
|
76
91
|
"world_writable": self.__world_writable,
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_dict(cls, acl_dict: dict) -> ACL:
|
|
96
|
+
"""
|
|
97
|
+
Create an ACL instance from a dictionary.
|
|
98
|
+
"""
|
|
99
|
+
user = User(acl_dict["user"])
|
|
100
|
+
group = Group.get(acl_dict["group"]) if "group" in acl_dict else None
|
|
101
|
+
acl = cls(user=user, group=group)
|
|
102
|
+
acl.__group_can_read = acl_dict.get("group_can_read", False)
|
|
103
|
+
acl.__group_can_write = acl_dict.get("group_can_write", False)
|
|
104
|
+
acl.__user_can_read = acl_dict.get("user_can_read", False)
|
|
105
|
+
acl.__user_can_write = acl_dict.get("user_can_write", False)
|
|
106
|
+
acl.__world_readable = acl_dict.get("world_readable", False)
|
|
107
|
+
acl.__world_writable = acl_dict.get("world_writable", False)
|
|
108
|
+
acl._set_permissions_string()
|
|
109
|
+
return acl
|
|
110
|
+
|
|
79
111
|
def to_json(self):
|
|
80
112
|
return json.dumps(self.to_dict())
|
datatailr/blob.py
CHANGED
|
@@ -12,12 +12,13 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import tempfile
|
|
14
14
|
|
|
15
|
-
from datatailr import dt__Blob
|
|
15
|
+
from datatailr.wrapper import dt__Blob
|
|
16
16
|
|
|
17
|
+
# Datatailr Blob API Client
|
|
18
|
+
__client__ = dt__Blob()
|
|
17
19
|
|
|
18
|
-
class Blob:
|
|
19
|
-
__blob_storage__ = dt__Blob()
|
|
20
20
|
|
|
21
|
+
class Blob:
|
|
21
22
|
def ls(self, path: str) -> list[str]:
|
|
22
23
|
"""
|
|
23
24
|
List files in the specified path.
|
|
@@ -25,7 +26,7 @@ class Blob:
|
|
|
25
26
|
:param path: The path to list files from.
|
|
26
27
|
:return: A list of file names in the specified path.
|
|
27
28
|
"""
|
|
28
|
-
return
|
|
29
|
+
return __client__.ls(path)
|
|
29
30
|
|
|
30
31
|
def get_file(self, name: str, path: str):
|
|
31
32
|
"""
|
|
@@ -35,7 +36,7 @@ class Blob:
|
|
|
35
36
|
name (str): The name of the blob to retrieve.
|
|
36
37
|
path (str): The path to store the blob as a file.
|
|
37
38
|
"""
|
|
38
|
-
return
|
|
39
|
+
return __client__.cp(f"blob://{name}", path)
|
|
39
40
|
|
|
40
41
|
def put_file(self, name: str, path: str):
|
|
41
42
|
"""
|
|
@@ -45,7 +46,7 @@ class Blob:
|
|
|
45
46
|
name (str): The name of the blob to create.
|
|
46
47
|
path (str): The path of the local file to copy.
|
|
47
48
|
"""
|
|
48
|
-
return
|
|
49
|
+
return __client__.cp(path, f"blob://{name}")
|
|
49
50
|
|
|
50
51
|
def exists(self, name: str) -> bool:
|
|
51
52
|
"""
|
|
@@ -57,7 +58,7 @@ class Blob:
|
|
|
57
58
|
Returns:
|
|
58
59
|
bool: True if the blob exists, False otherwise.
|
|
59
60
|
"""
|
|
60
|
-
return
|
|
61
|
+
return __client__.exists(name)
|
|
61
62
|
|
|
62
63
|
def delete(self, name: str):
|
|
63
64
|
"""
|
|
@@ -66,7 +67,7 @@ class Blob:
|
|
|
66
67
|
Args:
|
|
67
68
|
name (str): The name of the blob to delete.
|
|
68
69
|
"""
|
|
69
|
-
return
|
|
70
|
+
return __client__.rm(name)
|
|
70
71
|
|
|
71
72
|
def get_blob(self, name: str):
|
|
72
73
|
"""
|
|
@@ -81,9 +82,9 @@ class Blob:
|
|
|
81
82
|
# Since direct reading and writting of blobs is not implemented yet, we are using a temporary file.
|
|
82
83
|
# This is a workaround to allow reading the blob content directly from the blob storage.
|
|
83
84
|
|
|
84
|
-
with tempfile.NamedTemporaryFile(delete=
|
|
85
|
+
with tempfile.NamedTemporaryFile(delete=True) as temp_file:
|
|
85
86
|
self.get_file(name, temp_file.name)
|
|
86
|
-
with open(temp_file.name, "
|
|
87
|
+
with open(temp_file.name, "r") as f:
|
|
87
88
|
return f.read()
|
|
88
89
|
|
|
89
90
|
def put_blob(self, name: str, blob):
|
|
@@ -96,8 +97,7 @@ class Blob:
|
|
|
96
97
|
"""
|
|
97
98
|
# Since direct reading and writting of blobs is not implemented yet, we are using a temporary file.
|
|
98
99
|
# This is a workaround to allow writing the blob content directly to the blob storage.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
with open(temp_file.name, "wb") as f:
|
|
100
|
+
with tempfile.NamedTemporaryFile(delete=True) as temp_file:
|
|
101
|
+
with open(temp_file.name, "w") as f:
|
|
102
102
|
f.write(blob)
|
|
103
103
|
self.put_file(name, temp_file.name)
|
datatailr/build/image.py
CHANGED
|
@@ -25,9 +25,13 @@ class Image:
|
|
|
25
25
|
def __init__(
|
|
26
26
|
self,
|
|
27
27
|
acl: Optional[ACL] = None,
|
|
28
|
-
python_requirements: str = "",
|
|
28
|
+
python_requirements: str | list[str] = "",
|
|
29
29
|
build_script_pre: str = "",
|
|
30
30
|
build_script_post: str = "",
|
|
31
|
+
branch_name: Optional[str] = None,
|
|
32
|
+
commit_hash: Optional[str] = None,
|
|
33
|
+
path_to_repo: Optional[str] = None,
|
|
34
|
+
path_to_module: Optional[str] = None,
|
|
31
35
|
):
|
|
32
36
|
if acl is None:
|
|
33
37
|
signed_user = User.signed_user()
|
|
@@ -39,9 +43,11 @@ class Image:
|
|
|
39
43
|
raise TypeError("acl must be an instance of ACL.")
|
|
40
44
|
self.acl = acl or ACL(signed_user)
|
|
41
45
|
|
|
42
|
-
if os.path.isfile(python_requirements):
|
|
46
|
+
if isinstance(python_requirements, str) and os.path.isfile(python_requirements):
|
|
43
47
|
with open(python_requirements, "r") as f:
|
|
44
48
|
python_requirements = f.read()
|
|
49
|
+
elif isinstance(python_requirements, list):
|
|
50
|
+
python_requirements = "\n".join(python_requirements)
|
|
45
51
|
if not isinstance(python_requirements, str):
|
|
46
52
|
raise TypeError(
|
|
47
53
|
"python_requirements must be a string or a file path to a requirements file."
|
|
@@ -65,10 +71,36 @@ class Image:
|
|
|
65
71
|
"build_script_post must be a string or a file path to a script file."
|
|
66
72
|
)
|
|
67
73
|
self.build_script_post = build_script_post
|
|
74
|
+
self.branch_name = branch_name
|
|
75
|
+
self.commit_hash = commit_hash
|
|
76
|
+
self.path_to_repo = path_to_repo
|
|
77
|
+
self.path_to_module = path_to_module
|
|
68
78
|
|
|
69
79
|
def __repr__(self):
|
|
70
80
|
return f"Image(acl={self.acl},)"
|
|
71
81
|
|
|
82
|
+
def update(self, **kwargs):
|
|
83
|
+
for key, value in kwargs.items():
|
|
84
|
+
if key == "acl" and not isinstance(value, ACL):
|
|
85
|
+
raise TypeError("acl must be an instance of ACL.")
|
|
86
|
+
elif key == "python_requirements" and not isinstance(value, str):
|
|
87
|
+
raise TypeError("python_requirements must be a string.")
|
|
88
|
+
elif key == "build_script_pre" and not isinstance(value, str):
|
|
89
|
+
raise TypeError("build_script_pre must be a string.")
|
|
90
|
+
elif key == "build_script_post" and not isinstance(value, str):
|
|
91
|
+
raise TypeError("build_script_post must be a string.")
|
|
92
|
+
elif (
|
|
93
|
+
key in ["branch_name", "commit_hash", "path_to_repo", "path_to_module"]
|
|
94
|
+
and value is not None
|
|
95
|
+
and not isinstance(value, str)
|
|
96
|
+
):
|
|
97
|
+
raise TypeError(f"{key} must be a string or None.")
|
|
98
|
+
if key not in self.__dict__:
|
|
99
|
+
raise AttributeError(
|
|
100
|
+
f"'{self.__class__.__name__}' object has no attribute '{key}'"
|
|
101
|
+
)
|
|
102
|
+
setattr(self, key, value)
|
|
103
|
+
|
|
72
104
|
def to_dict(self):
|
|
73
105
|
"""
|
|
74
106
|
Convert the Image instance to a dictionary representation.
|
|
@@ -78,6 +110,10 @@ class Image:
|
|
|
78
110
|
"python_requirements": self.python_requirements,
|
|
79
111
|
"build_script_pre": self.build_script_pre,
|
|
80
112
|
"build_script_post": self.build_script_post,
|
|
113
|
+
"branch_name": self.branch_name,
|
|
114
|
+
"commit_hash": self.commit_hash,
|
|
115
|
+
"path_to_repo": self.path_to_repo,
|
|
116
|
+
"path_to_module": self.path_to_module,
|
|
81
117
|
}
|
|
82
118
|
|
|
83
119
|
def to_json(self):
|
datatailr/dt_json.py
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import importlib
|
|
12
12
|
import json
|
|
13
|
+
import base64
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class dt_json:
|
|
@@ -40,3 +41,34 @@ class dt_json:
|
|
|
40
41
|
@classmethod
|
|
41
42
|
def load(cls, json_file):
|
|
42
43
|
return json.load(json_file)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def encode_json(obj) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Encode an object to a JSON string with class information.
|
|
49
|
+
"""
|
|
50
|
+
json_str = json.dumps(
|
|
51
|
+
obj, default=lambda o: o.__dict__ if hasattr(o, "__dict__") else o
|
|
52
|
+
)
|
|
53
|
+
return base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_base64(s: str) -> bool:
|
|
57
|
+
try:
|
|
58
|
+
# Try decoding the string
|
|
59
|
+
base64.b64decode(s, validate=True)
|
|
60
|
+
return True
|
|
61
|
+
except (ValueError, TypeError):
|
|
62
|
+
# If decoding fails, it's not valid Base64
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def decode_json(encoded_str: str):
|
|
67
|
+
"""
|
|
68
|
+
Decode a JSON string back to an object.
|
|
69
|
+
"""
|
|
70
|
+
if is_base64(encoded_str):
|
|
71
|
+
json_str = base64.b64decode(encoded_str.encode("utf-8")).decode("utf-8")
|
|
72
|
+
else:
|
|
73
|
+
json_str = encoded_str
|
|
74
|
+
return json.loads(json_str)
|
datatailr/errors.py
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
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
|
+
|
|
1
12
|
class DatatailrError(Exception):
|
|
2
13
|
"""Base class for all DataTailr exceptions."""
|
|
3
14
|
|
|
@@ -8,3 +19,9 @@ class BatchJobError(DatatailrError):
|
|
|
8
19
|
"""Exception raised for errors related to batch jobs."""
|
|
9
20
|
|
|
10
21
|
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ScheduleError(DatatailrError):
|
|
25
|
+
"""Exception raised for errors related to scheduling."""
|
|
26
|
+
|
|
27
|
+
pass
|
datatailr/group.py
CHANGED
|
@@ -10,17 +10,25 @@
|
|
|
10
10
|
|
|
11
11
|
from typing import Optional, Union
|
|
12
12
|
|
|
13
|
-
from datatailr import dt__Group
|
|
13
|
+
from datatailr.wrapper import dt__Group
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Datatailr Group API Client
|
|
17
|
+
__client__ = dt__Group()
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
class Group:
|
|
17
|
-
"""
|
|
21
|
+
"""
|
|
22
|
+
Representing a Datatailr Group.
|
|
23
|
+
|
|
18
24
|
This class provides methods to interact with the Datatailr Group API.
|
|
19
25
|
It allows you to create, update, delete, and manage groups within the Datatailr platform.
|
|
26
|
+
|
|
20
27
|
Attributes:
|
|
21
28
|
name (str): The name of the group.
|
|
22
29
|
members (list): A list of members in the group.
|
|
23
30
|
group_id (int): The unique identifier for the group.
|
|
31
|
+
|
|
24
32
|
Static Methods:
|
|
25
33
|
add(name: str) -> 'Group':
|
|
26
34
|
Create a new group with the specified name.
|
|
@@ -32,6 +40,7 @@ class Group:
|
|
|
32
40
|
Remove a group by its name.
|
|
33
41
|
exists(name: str) -> bool:
|
|
34
42
|
Check if a group exists by its name.
|
|
43
|
+
|
|
35
44
|
Instance Methods:
|
|
36
45
|
add_users(usernames: list) -> None:
|
|
37
46
|
Add users to the group.
|
|
@@ -39,9 +48,6 @@ class Group:
|
|
|
39
48
|
Remove users from the group.
|
|
40
49
|
"""
|
|
41
50
|
|
|
42
|
-
# Datatailr Group API Client
|
|
43
|
-
__client__ = dt__Group()
|
|
44
|
-
|
|
45
51
|
def __init__(self, name: str):
|
|
46
52
|
self.__name: str = name
|
|
47
53
|
self.__members: list = []
|
|
@@ -69,9 +75,7 @@ class Group:
|
|
|
69
75
|
def __refresh__(self):
|
|
70
76
|
if not self.name:
|
|
71
77
|
raise ValueError("Name is not set. Cannot refresh group.")
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
group = Group.__client__.get(self.name)
|
|
78
|
+
group = __client__.get(self.name)
|
|
75
79
|
if group:
|
|
76
80
|
self.__name = group["name"]
|
|
77
81
|
self.__members = group["members"]
|
|
@@ -99,33 +103,33 @@ class Group:
|
|
|
99
103
|
|
|
100
104
|
@staticmethod
|
|
101
105
|
def add(name: str) -> Optional["Group"]:
|
|
102
|
-
|
|
106
|
+
__client__.add(name)
|
|
103
107
|
return Group.get(name)
|
|
104
108
|
|
|
105
109
|
@staticmethod
|
|
106
110
|
def ls() -> list:
|
|
107
|
-
groups =
|
|
111
|
+
groups = __client__.ls()
|
|
108
112
|
return [Group.get(group["name"]) for group in groups]
|
|
109
113
|
|
|
110
114
|
@staticmethod
|
|
111
115
|
def remove(name: str) -> None:
|
|
112
|
-
|
|
116
|
+
__client__.rm(name)
|
|
113
117
|
return None
|
|
114
118
|
|
|
115
119
|
@staticmethod
|
|
116
120
|
def exists(name: str) -> bool:
|
|
117
|
-
return
|
|
121
|
+
return __client__.exists(name)
|
|
118
122
|
|
|
119
123
|
def add_users(self, usernames: list) -> None:
|
|
120
124
|
if not self.name:
|
|
121
125
|
raise ValueError("Name is not set. Cannot add users.")
|
|
122
|
-
|
|
126
|
+
__client__.add_users(self.name, ",".join(usernames))
|
|
123
127
|
self.__refresh__()
|
|
124
128
|
|
|
125
129
|
def remove_users(self, usernames: list) -> None:
|
|
126
130
|
if not self.name:
|
|
127
131
|
raise ValueError("Name is not set. Cannot remove users.")
|
|
128
|
-
|
|
132
|
+
__client__.rm_users(self.name, ",".join(usernames))
|
|
129
133
|
self.__refresh__()
|
|
130
134
|
|
|
131
135
|
def is_member(self, user) -> bool:
|
datatailr/logging.py
CHANGED
|
@@ -13,6 +13,8 @@ import os
|
|
|
13
13
|
from logging import StreamHandler
|
|
14
14
|
from logging.handlers import RotatingFileHandler
|
|
15
15
|
from typing import Optional
|
|
16
|
+
from datatailr import User
|
|
17
|
+
from datatailr.wrapper import dt__Tag
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def get_log_level() -> int:
|
|
@@ -31,12 +33,28 @@ def get_log_level() -> int:
|
|
|
31
33
|
return logging.INFO
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
tag = dt__Tag()
|
|
37
|
+
node_name = tag.get("node_name") or "local"
|
|
38
|
+
node_ip = tag.get("node_ip")
|
|
39
|
+
job_name = os.getenv("DATATAILR_JOB_NAME", "unknown_job")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
user = User.signed_user().name
|
|
43
|
+
except Exception:
|
|
44
|
+
import getpass
|
|
45
|
+
|
|
46
|
+
user = getpass.getuser()
|
|
47
|
+
|
|
48
|
+
LOG_FORMAT = f"%(asctime)s - %(levelname)s - {node_name}:{node_ip} - {user} - {job_name} - %(name)s - [Line %(lineno)d]: %(message)s"
|
|
49
|
+
|
|
50
|
+
|
|
34
51
|
class DatatailrLogger:
|
|
35
52
|
def __init__(
|
|
36
53
|
self,
|
|
37
54
|
name: str,
|
|
38
55
|
log_file: Optional[str] = None,
|
|
39
56
|
log_level: int = get_log_level(),
|
|
57
|
+
log_format: str = LOG_FORMAT,
|
|
40
58
|
):
|
|
41
59
|
"""
|
|
42
60
|
Initialize the DatatailrLogger.
|
|
@@ -51,9 +69,7 @@ class DatatailrLogger:
|
|
|
51
69
|
# Stream handler for stdout/stderr
|
|
52
70
|
stream_handler = StreamHandler()
|
|
53
71
|
stream_handler.setLevel(log_level)
|
|
54
|
-
stream_formatter = logging.Formatter(
|
|
55
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
56
|
-
)
|
|
72
|
+
stream_formatter = logging.Formatter(log_format)
|
|
57
73
|
stream_handler.setFormatter(stream_formatter)
|
|
58
74
|
self.logger.addHandler(stream_handler)
|
|
59
75
|
|
|
@@ -63,9 +79,7 @@ class DatatailrLogger:
|
|
|
63
79
|
log_file, maxBytes=10 * 1024 * 1024, backupCount=5
|
|
64
80
|
)
|
|
65
81
|
file_handler.setLevel(log_level)
|
|
66
|
-
file_formatter = logging.Formatter(
|
|
67
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
68
|
-
)
|
|
82
|
+
file_formatter = logging.Formatter(log_format)
|
|
69
83
|
file_handler.setFormatter(file_formatter)
|
|
70
84
|
self.logger.addHandler(file_handler)
|
|
71
85
|
self.enable_opentelemetry()
|
|
@@ -86,8 +100,5 @@ class DatatailrLogger:
|
|
|
86
100
|
from opentelemetry.instrumentation.logging import LoggingInstrumentor # type: ignore
|
|
87
101
|
|
|
88
102
|
LoggingInstrumentor().instrument(set_logging_format=True)
|
|
89
|
-
self.logger.debug("OpenTelemetry logging instrumentation enabled.")
|
|
90
103
|
except ImportError:
|
|
91
|
-
|
|
92
|
-
"OpenTelemetry is not installed. Proceeding without instrumentation."
|
|
93
|
-
)
|
|
104
|
+
pass # OpenTelemetry is not installed, skip instrumentation
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
|
|
14
|
+
# The purpose of this script is to be the entrypoint for all jobs running on datatailr.
|
|
15
|
+
# The main functions of the script are:
|
|
16
|
+
# 1. Create a linux user and group for the job.
|
|
17
|
+
# 2. Set the environment variables for the job.
|
|
18
|
+
# 3. Run the job in a separate process, as the newly created user and pass all relevant environment variables.
|
|
19
|
+
# There are muliple environment variables which are required for the job to run.
|
|
20
|
+
# Some of them are necessary for the setup stage, which is executed directly in this script as the linux root user.
|
|
21
|
+
# Others are passed to the job script, which is executed in a separate process with only the users' privileges and not as a root user.
|
|
22
|
+
#
|
|
23
|
+
# Setup environment variables:
|
|
24
|
+
# DATATAILR_USER - the user under which the job will run.
|
|
25
|
+
# DATATAILR_GROUP - the group under which the job will run.
|
|
26
|
+
# DATATAILR_UID - the user ID of the user as it is defined in the system.
|
|
27
|
+
# DATATAILR_GID - the group ID of the group as it is defined in the system.
|
|
28
|
+
# DATATAILR_JOB_TYPE - the type of job to run. (batch\service\app\excel\IDE)
|
|
29
|
+
# Job environment variables (not all are always relevant, depending on the job type):
|
|
30
|
+
# DATATAILR_JOB_ARGUMENT_MAPPING - a JSON string mapping job argument names to their
|
|
31
|
+
# DATATAILR_BATCH_RUN_ID - the unique identifier for the batch run.
|
|
32
|
+
# DATATAILR_BATCH_ID - the unique identifier for the batch.
|
|
33
|
+
# DATATAILR_JOB_ID - the unique identifier for the job.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
from typing import Tuple
|
|
38
|
+
from datatailr.logging import DatatailrLogger
|
|
39
|
+
from datatailr.dt_json import encode_json
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
logger = DatatailrLogger(os.path.abspath(__file__)).get_logger()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_env_var(name: str, default: str | None = None) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Get an environment variable.
|
|
48
|
+
If the variable is not set, raise an error.
|
|
49
|
+
"""
|
|
50
|
+
if name not in os.environ:
|
|
51
|
+
if default is not None:
|
|
52
|
+
return default
|
|
53
|
+
logger.error(f"Environment variable '{name}' is not set.")
|
|
54
|
+
raise ValueError(f"Environment variable '{name}' is not set.")
|
|
55
|
+
return os.environ[name]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def create_user_and_group() -> Tuple[str, str]:
|
|
59
|
+
"""
|
|
60
|
+
Create a user and group for the job.
|
|
61
|
+
The user and group names are taken from the environment variables DATATAILR_USER and DATATAILR_GROUP.
|
|
62
|
+
The group and user are created with the same uid and gid as passed in the environment variables DATATAILR_UID and DATATAILR_GID.
|
|
63
|
+
If the user or group already exists, do nothing.
|
|
64
|
+
"""
|
|
65
|
+
user = get_env_var("DATATAILR_USER")
|
|
66
|
+
group = get_env_var("DATATAILR_GROUP")
|
|
67
|
+
uid = get_env_var("DATATAILR_UID")
|
|
68
|
+
gid = get_env_var("DATATAILR_GID")
|
|
69
|
+
|
|
70
|
+
# Create group if it does not exist
|
|
71
|
+
os.system(f"getent group {group} || groupadd {group} -g {gid} -o")
|
|
72
|
+
|
|
73
|
+
# Create user if it does not exist
|
|
74
|
+
os.system(
|
|
75
|
+
f"getent passwd {user} || useradd -g {group} -s /bin/bash -m {user} -u {uid} -o"
|
|
76
|
+
)
|
|
77
|
+
return user, group
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_command_as_user(command: str, user: str, env_vars: dict):
|
|
81
|
+
"""
|
|
82
|
+
Run a command as a specific user with the given environment variables.
|
|
83
|
+
"""
|
|
84
|
+
env_vars.update({"PATH": get_env_var("PATH")})
|
|
85
|
+
env_vars_str = " ".join(f"{key}='{value}'" for key, value in env_vars.items())
|
|
86
|
+
full_command = f"sudo -u {user} {env_vars_str} {command}"
|
|
87
|
+
logger.debug(f"Running command: {full_command}")
|
|
88
|
+
os.system(full_command)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
user, _ = create_user_and_group()
|
|
93
|
+
job_type = get_env_var("DATATAILR_JOB_TYPE")
|
|
94
|
+
|
|
95
|
+
job_name = get_env_var("DATATAILR_JOB_NAME")
|
|
96
|
+
job_id = get_env_var("DATATAILR_JOB_ID")
|
|
97
|
+
entrypoint = get_env_var("DATATAILR_ENTRYPOINT")
|
|
98
|
+
|
|
99
|
+
if job_type == "batch":
|
|
100
|
+
run_id = get_env_var("DATATAILR_BATCH_RUN_ID")
|
|
101
|
+
batch_id = get_env_var("DATATAILR_BATCH_ID")
|
|
102
|
+
job_argument_mapping = get_env_var(
|
|
103
|
+
"DATATAILR_JOB_ARGUMENT_MAPPING", encode_json({})
|
|
104
|
+
)
|
|
105
|
+
env = {
|
|
106
|
+
"DATATAILR_BATCH_RUN_ID": run_id,
|
|
107
|
+
"DATATAILR_BATCH_ID": batch_id,
|
|
108
|
+
"DATATAILR_JOB_ID": job_id,
|
|
109
|
+
"DATATAILR_BATCH_ENTRYPOINT": entrypoint,
|
|
110
|
+
"DATATAILR_JOB_ARGUMENT_MAPPING": job_argument_mapping,
|
|
111
|
+
}
|
|
112
|
+
run_command_as_user("datatailr_run_batch", user, env)
|
|
113
|
+
elif job_type == "service":
|
|
114
|
+
env = {
|
|
115
|
+
"DATATAILR_JOB_NAME": job_name,
|
|
116
|
+
"DATATAILR_JOB_ID": job_id,
|
|
117
|
+
"DATATAILR_ENTRYPOINT": entrypoint,
|
|
118
|
+
}
|
|
119
|
+
run_command_as_user("datatailr_run_service", user, env)
|
|
120
|
+
elif job_type == "app":
|
|
121
|
+
env = {
|
|
122
|
+
"DATATAILR_JOB_NAME": job_name,
|
|
123
|
+
"DATATAILR_JOB_ID": job_id,
|
|
124
|
+
"DATATAILR_ENTRYPOINT": entrypoint,
|
|
125
|
+
}
|
|
126
|
+
run_command_as_user("datatailr_run_app", user, env)
|
|
127
|
+
elif job_type == "excel":
|
|
128
|
+
env = {
|
|
129
|
+
"DATATAILR_JOB_NAME": job_name,
|
|
130
|
+
"DATATAILR_JOB_ID": job_id,
|
|
131
|
+
"DATATAILR_ENTRYPOINT": entrypoint,
|
|
132
|
+
}
|
|
133
|
+
run_command_as_user("datatailr_run_excel", user, env)
|
|
134
|
+
elif job_type == "IDE":
|
|
135
|
+
pass
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(f"Unknown job type: {job_type}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
try:
|
|
142
|
+
logger.debug("Starting job execution...")
|
|
143
|
+
main()
|
|
144
|
+
logger.debug("Job executed successfully.")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Error during job execution: {e}")
|
|
147
|
+
raise
|