datatailr 0.1.0__tar.gz → 0.1.2__tar.gz

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.

Files changed (42) hide show
  1. {datatailr-0.1.0 → datatailr-0.1.2}/LICENSE +0 -1
  2. datatailr-0.1.2/PKG-INFO +24 -0
  3. datatailr-0.1.2/README.md +5 -0
  4. datatailr-0.1.2/pyproject.toml +43 -0
  5. datatailr-0.1.2/setup.py +10 -0
  6. datatailr-0.1.2/src/datatailr/__init__.py +63 -0
  7. datatailr-0.1.2/src/datatailr/acl.py +80 -0
  8. datatailr-0.1.2/src/datatailr/blob.py +103 -0
  9. datatailr-0.1.2/src/datatailr/build/__init__.py +11 -0
  10. datatailr-0.1.2/src/datatailr/build/image.py +87 -0
  11. datatailr-0.1.2/src/datatailr/dt_json.py +42 -0
  12. datatailr-0.1.2/src/datatailr/errors.py +10 -0
  13. datatailr-0.1.2/src/datatailr/group.py +136 -0
  14. datatailr-0.1.2/src/datatailr/logging.py +93 -0
  15. datatailr-0.1.2/src/datatailr/scheduler/__init__.py +38 -0
  16. datatailr-0.1.2/src/datatailr/scheduler/arguments_cache.py +126 -0
  17. datatailr-0.1.2/src/datatailr/scheduler/base.py +238 -0
  18. datatailr-0.1.2/src/datatailr/scheduler/batch.py +347 -0
  19. datatailr-0.1.2/src/datatailr/scheduler/batch_decorator.py +112 -0
  20. datatailr-0.1.2/src/datatailr/scheduler/constants.py +20 -0
  21. datatailr-0.1.2/src/datatailr/scheduler/utils.py +28 -0
  22. datatailr-0.1.2/src/datatailr/user.py +201 -0
  23. datatailr-0.1.2/src/datatailr/utils.py +35 -0
  24. datatailr-0.1.2/src/datatailr/version.py +14 -0
  25. datatailr-0.1.2/src/datatailr/wrapper.py +204 -0
  26. datatailr-0.1.2/src/datatailr.egg-info/PKG-INFO +24 -0
  27. datatailr-0.1.2/src/datatailr.egg-info/SOURCES.txt +33 -0
  28. datatailr-0.1.2/src/datatailr.egg-info/entry_points.txt +2 -0
  29. datatailr-0.1.2/src/datatailr.egg-info/requires.txt +7 -0
  30. datatailr-0.1.2/src/datatailr.egg-info/top_level.txt +2 -0
  31. datatailr-0.1.2/src/sbin/run_job.py +63 -0
  32. datatailr-0.1.2/src/test_module/__init__.py +17 -0
  33. datatailr-0.1.2/src/test_module/test_submodule.py +38 -0
  34. datatailr-0.1.0/PKG-INFO +0 -17
  35. datatailr-0.1.0/README.md +0 -3
  36. datatailr-0.1.0/pyproject.toml +0 -19
  37. datatailr-0.1.0/src/datatailr/__init__.py +0 -0
  38. datatailr-0.1.0/src/datatailr.egg-info/PKG-INFO +0 -17
  39. datatailr-0.1.0/src/datatailr.egg-info/SOURCES.txt +0 -8
  40. datatailr-0.1.0/src/datatailr.egg-info/top_level.txt +0 -1
  41. {datatailr-0.1.0 → datatailr-0.1.2}/setup.cfg +0 -0
  42. {datatailr-0.1.0 → datatailr-0.1.2}/src/datatailr.egg-info/dependency_links.txt +0 -0
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: datatailr
3
+ Version: 0.1.2
4
+ Summary: The datatailr package
5
+ Author-email: Datatailr <pypi@datatailr.com>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Provides-Extra: dev
13
+ Requires-Dist: ruff; extra == "dev"
14
+ Requires-Dist: pre-commit; extra == "dev"
15
+ Requires-Dist: mypy; extra == "dev"
16
+ Requires-Dist: types-setuptools; extra == "dev"
17
+ Requires-Dist: toml; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # datatailr
21
+
22
+ Datatailr empowers your team to streamline analytics and data workflows from idea to production without infrastructure hurdles.
23
+
24
+ Visit [our website](https://www.datatailr.com/) for more!
@@ -0,0 +1,5 @@
1
+ # datatailr
2
+
3
+ Datatailr empowers your team to streamline analytics and data workflows from idea to production without infrastructure hurdles.
4
+
5
+ Visit [our website](https://www.datatailr.com/) for more!
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=60", "wheel", "toml"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "datatailr"
7
+ version = "0.1.2"
8
+ description = "The datatailr package"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ authors = [
12
+ { name="Datatailr", email="pypi@datatailr.com" }
13
+ ]
14
+ license = "MIT"
15
+ license-files = [
16
+ "LICENSE",
17
+ ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "Operating System :: OS Independent"
21
+ ]
22
+
23
+ [project.scripts]
24
+ run_dt_job = "datatailr.sbin.run_job:main"
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "ruff",
29
+ "pre-commit",
30
+ "mypy",
31
+ "types-setuptools",
32
+ "toml",
33
+ ]
34
+
35
+ [tool.ruff]
36
+ src = [
37
+ "src",
38
+ "../../tests/src/python"
39
+ ]
40
+ lint.ignore = ["F841"]
41
+ show-fixes = true
42
+ fix = true
43
+ output-format = "full"
@@ -0,0 +1,10 @@
1
+ from datatailr import __version__ # type: ignore
2
+ from setuptools import find_packages, setup
3
+
4
+ setup(
5
+ name="datatailr",
6
+ version=__version__,
7
+ packages=find_packages(where="src"),
8
+ package_dir={"": "src"},
9
+ data_files=[("/datatailr/sbin", ["src/sbin/run_job.py"])],
10
+ )
@@ -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
+ ]
@@ -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())
@@ -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)
@@ -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)
@@ -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
@@ -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
+ )