datatailr 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of datatailr might be problematic. Click here for more details.
- datatailr/__init__.py +63 -0
- datatailr/acl.py +80 -0
- datatailr/blob.py +103 -0
- datatailr/build/__init__.py +11 -0
- datatailr/build/image.py +87 -0
- datatailr/dt_json.py +42 -0
- datatailr/errors.py +10 -0
- datatailr/group.py +136 -0
- datatailr/logging.py +93 -0
- datatailr/sbin/run_job.py +63 -0
- datatailr/scheduler/__init__.py +38 -0
- datatailr/scheduler/arguments_cache.py +126 -0
- datatailr/scheduler/base.py +238 -0
- datatailr/scheduler/batch.py +350 -0
- datatailr/scheduler/batch_decorator.py +112 -0
- datatailr/scheduler/constants.py +20 -0
- datatailr/scheduler/utils.py +28 -0
- datatailr/user.py +201 -0
- datatailr/utils.py +35 -0
- datatailr/version.py +14 -0
- datatailr/wrapper.py +204 -0
- {datatailr-0.1.1.dist-info → datatailr-0.1.3.dist-info}/METADATA +8 -3
- datatailr-0.1.3.dist-info/RECORD +29 -0
- {datatailr-0.1.1.dist-info → datatailr-0.1.3.dist-info}/WHEEL +1 -1
- datatailr-0.1.3.dist-info/entry_points.txt +2 -0
- datatailr-0.1.3.dist-info/top_level.txt +2 -0
- test_module/__init__.py +17 -0
- test_module/test_submodule.py +38 -0
- datatailr-0.1.1.dist-info/RECORD +0 -6
- datatailr-0.1.1.dist-info/top_level.txt +0 -1
- {datatailr-0.1.1.dist-info → datatailr-0.1.3.dist-info}/licenses/LICENSE +0 -0
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
|
datatailr/build/image.py
ADDED
|
@@ -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
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
|