pyfacl 1.0.0__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.
- pyfacl-1.0.0/LICENSE +21 -0
- pyfacl-1.0.0/PKG-INFO +65 -0
- pyfacl-1.0.0/README.md +44 -0
- pyfacl-1.0.0/pyfacl/__init__.py +3 -0
- pyfacl-1.0.0/pyfacl/logger.py +29 -0
- pyfacl-1.0.0/pyfacl/pyfacl.py +306 -0
- pyfacl-1.0.0/pyproject.toml +37 -0
pyfacl-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sail-mskcc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyfacl-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyfacl
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Package to manage access control using POSIX ACLs
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: tobiaspk
|
|
7
|
+
Author-email: tobiaspk1@gmail.com
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: black (>=23.0.0) ; extra == "dev"
|
|
15
|
+
Requires-Dist: flake8 (>=6.0.0) ; extra == "dev"
|
|
16
|
+
Requires-Dist: isort (>=5.12.0) ; extra == "dev"
|
|
17
|
+
Requires-Dist: pre-commit (>=3.0.0) ; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# PyFACL
|
|
22
|
+
|
|
23
|
+
A Python library for parsing and checking POSIX File Access Control Lists (FACL).
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### From PyPI
|
|
28
|
+
```bash
|
|
29
|
+
pip install pyfacl
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from pyfacl import FACL
|
|
36
|
+
|
|
37
|
+
# Initialize and parse FACL for a file/directory
|
|
38
|
+
facl = FACL()
|
|
39
|
+
facl.parse("/path/to/file")
|
|
40
|
+
|
|
41
|
+
# Check permissions with different modes
|
|
42
|
+
facl.has_permission("user:username:r-x", mode="exact") # exact match
|
|
43
|
+
facl.has_permission("user:username:r--", mode="at_least") # has at least read
|
|
44
|
+
facl.has_permission("user:username:rwx", mode="at_most") # has at most rwx
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Permission Modes
|
|
48
|
+
|
|
49
|
+
- **`exact`**: Permissions must match exactly
|
|
50
|
+
- **`at_least`**: Must have at least the specified permissions
|
|
51
|
+
- **`at_most`**: Must have at most the specified permissions
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
### Setup Development Environment
|
|
56
|
+
```bash
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
pre-commit install
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Run Pre-commit Checks
|
|
62
|
+
```bash
|
|
63
|
+
pre-commit run --all-files
|
|
64
|
+
```
|
|
65
|
+
|
pyfacl-1.0.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# PyFACL
|
|
2
|
+
|
|
3
|
+
A Python library for parsing and checking POSIX File Access Control Lists (FACL).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### From PyPI
|
|
8
|
+
```bash
|
|
9
|
+
pip install pyfacl
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from pyfacl import FACL
|
|
16
|
+
|
|
17
|
+
# Initialize and parse FACL for a file/directory
|
|
18
|
+
facl = FACL()
|
|
19
|
+
facl.parse("/path/to/file")
|
|
20
|
+
|
|
21
|
+
# Check permissions with different modes
|
|
22
|
+
facl.has_permission("user:username:r-x", mode="exact") # exact match
|
|
23
|
+
facl.has_permission("user:username:r--", mode="at_least") # has at least read
|
|
24
|
+
facl.has_permission("user:username:rwx", mode="at_most") # has at most rwx
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Permission Modes
|
|
28
|
+
|
|
29
|
+
- **`exact`**: Permissions must match exactly
|
|
30
|
+
- **`at_least`**: Must have at least the specified permissions
|
|
31
|
+
- **`at_most`**: Must have at most the specified permissions
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
### Setup Development Environment
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
pre-commit install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Run Pre-commit Checks
|
|
42
|
+
```bash
|
|
43
|
+
pre-commit run --all-files
|
|
44
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def setup_logger(name: str, level: int = logging.INFO) -> logging.Logger:
|
|
5
|
+
"""
|
|
6
|
+
Set up and return a logger with the specified name and logging level.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
name (str): The name of the logger.
|
|
10
|
+
level (int): The logging level (default is logging.INFO).
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
logging.Logger: Configured logger instance.
|
|
14
|
+
"""
|
|
15
|
+
logger = logging.getLogger(name)
|
|
16
|
+
logger.setLevel(level)
|
|
17
|
+
|
|
18
|
+
if not logger.hasHandlers():
|
|
19
|
+
ch = logging.StreamHandler()
|
|
20
|
+
ch.setLevel(level)
|
|
21
|
+
|
|
22
|
+
formatter = logging.Formatter(
|
|
23
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
24
|
+
)
|
|
25
|
+
ch.setFormatter(formatter)
|
|
26
|
+
|
|
27
|
+
logger.addHandler(ch)
|
|
28
|
+
|
|
29
|
+
return logger
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from pyfacl import logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FACL:
|
|
8
|
+
"""
|
|
9
|
+
Represents a POSIX File Access Control List (FACL) for a given file or directory.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, v: int = 0, _facl: str = ""):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the FACL object. Args are used for debugging and testing.
|
|
15
|
+
"""
|
|
16
|
+
self.logger = logger.setup_logger(
|
|
17
|
+
__name__, level=logging.INFO if v == 0 else logging.DEBUG
|
|
18
|
+
)
|
|
19
|
+
self.is_init = False
|
|
20
|
+
self.facl = _facl
|
|
21
|
+
self.path = ""
|
|
22
|
+
self.owner = ""
|
|
23
|
+
self.group = ""
|
|
24
|
+
self.flags = ""
|
|
25
|
+
self.acls = []
|
|
26
|
+
|
|
27
|
+
def parse(self, path: str):
|
|
28
|
+
"""
|
|
29
|
+
Parse the FACL for the given file or directory path.
|
|
30
|
+
Args:
|
|
31
|
+
path (str): The file or directory path.
|
|
32
|
+
"""
|
|
33
|
+
self.is_init = True
|
|
34
|
+
self.facl = self._get_facl(path)
|
|
35
|
+
self._parse_metadata()
|
|
36
|
+
self._parse_acls()
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _facl_available():
|
|
40
|
+
"""
|
|
41
|
+
Check if the `getfacl` command is available on the system.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
bool: True if `getfacl` is available, False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
return (
|
|
47
|
+
subprocess.call(
|
|
48
|
+
["which", "getfacl"],
|
|
49
|
+
stdout=subprocess.DEVNULL,
|
|
50
|
+
stderr=subprocess.DEVNULL,
|
|
51
|
+
)
|
|
52
|
+
== 0
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _get_facl(self, path: str) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Retrieve the FACL for the given path using the `getfacl` command.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path (str): The file or directory path.
|
|
61
|
+
Returns:
|
|
62
|
+
str: The raw FACL output from the `getfacl` command.
|
|
63
|
+
"""
|
|
64
|
+
if not self._facl_available():
|
|
65
|
+
self.logger.error("The 'getfacl' command is not available on this system.")
|
|
66
|
+
return ""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["getfacl", path], capture_output=True, text=True, check=True
|
|
70
|
+
)
|
|
71
|
+
return result.stdout
|
|
72
|
+
except subprocess.CalledProcessError as e:
|
|
73
|
+
self.logger.error(f"Error retrieving FACL for {path}: {e}")
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
def _parse_metadata(self):
|
|
77
|
+
"""
|
|
78
|
+
Parse metadata, such as path, owner, groups and flags.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
```
|
|
82
|
+
# file: data1/collab002/sail/isabl/datalake/prod/010/collaborators
|
|
83
|
+
# owner: krauset
|
|
84
|
+
# group: grp_hpc_collab002
|
|
85
|
+
# flags: -s-
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
patterns = {
|
|
89
|
+
"path": "# file: ",
|
|
90
|
+
"owner": "# owner: ",
|
|
91
|
+
"group": "# group: ",
|
|
92
|
+
"flags": "# flags: ",
|
|
93
|
+
}
|
|
94
|
+
for key, pattern in patterns.items():
|
|
95
|
+
if pattern not in self.facl:
|
|
96
|
+
self.logger.warning(
|
|
97
|
+
f"Metadata pattern '{pattern}' not found in FACL output."
|
|
98
|
+
)
|
|
99
|
+
continue
|
|
100
|
+
for line in self.facl.splitlines():
|
|
101
|
+
if line.startswith(pattern):
|
|
102
|
+
setattr(self, key, line.split(":", 1)[1].strip())
|
|
103
|
+
|
|
104
|
+
def _parse_acl(self, acl_line: str):
|
|
105
|
+
"""
|
|
106
|
+
Parse a single ACL entry line.
|
|
107
|
+
Example entries:
|
|
108
|
+
```
|
|
109
|
+
user::rwx
|
|
110
|
+
user:krauset:rwx
|
|
111
|
+
group::r-x
|
|
112
|
+
group:grp_hpc_collab002:r-x
|
|
113
|
+
mask::rwx
|
|
114
|
+
other::r-x
|
|
115
|
+
default:user::rwx
|
|
116
|
+
default:group::r-x
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# parse
|
|
120
|
+
acl_split = acl_line.split(":")
|
|
121
|
+
if len(acl_split) not in [3, 4]:
|
|
122
|
+
msg = (
|
|
123
|
+
f"ACL line does not have the correct number of fields (3 or 4):"
|
|
124
|
+
f"\nLine: {acl_line}\nFields: {acl_split}"
|
|
125
|
+
)
|
|
126
|
+
self.logger.warning(msg)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# default
|
|
130
|
+
default = len(acl_split) == 4
|
|
131
|
+
if default:
|
|
132
|
+
if acl_split[0] not in ["d", "default"]:
|
|
133
|
+
msg = (
|
|
134
|
+
f"Unexpected default ACL prefix '{acl_split[0]}' in line:"
|
|
135
|
+
f"\n{acl_line}"
|
|
136
|
+
)
|
|
137
|
+
self.logger.warning(msg)
|
|
138
|
+
return None
|
|
139
|
+
acl_split = acl_split[1:] # remove default prefix
|
|
140
|
+
|
|
141
|
+
# type
|
|
142
|
+
type_map = {
|
|
143
|
+
"u": "user",
|
|
144
|
+
"user": "user",
|
|
145
|
+
"g": "group",
|
|
146
|
+
"group": "group",
|
|
147
|
+
"m": "mask",
|
|
148
|
+
"mask": "mask",
|
|
149
|
+
"o": "other",
|
|
150
|
+
"other": "other",
|
|
151
|
+
}
|
|
152
|
+
if acl_split[0] not in type_map:
|
|
153
|
+
self.logger.warning(
|
|
154
|
+
f"Unexpected ACL type '{acl_split[0]}' in line:\n{acl_line}"
|
|
155
|
+
)
|
|
156
|
+
return None
|
|
157
|
+
acl_type = type_map[acl_split[0]]
|
|
158
|
+
|
|
159
|
+
# name
|
|
160
|
+
name = acl_split[1]
|
|
161
|
+
if name == "":
|
|
162
|
+
if acl_type == "user":
|
|
163
|
+
if not self.is_init:
|
|
164
|
+
self.logger.warning("FACL not initialized before parsing ACLs.")
|
|
165
|
+
name = self.owner
|
|
166
|
+
elif acl_type == "group":
|
|
167
|
+
if not self.is_init:
|
|
168
|
+
self.logger.warning("FACL not initialized before parsing ACLs.")
|
|
169
|
+
name = self.group
|
|
170
|
+
|
|
171
|
+
# permissions
|
|
172
|
+
permissions = acl_split[2]
|
|
173
|
+
if not all(c in "rwx-" for c in permissions) or len(permissions) != 3:
|
|
174
|
+
self.logger.warning(
|
|
175
|
+
f"Invalid permissions '{permissions}' in line:\n{acl_line}"
|
|
176
|
+
)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# create
|
|
180
|
+
acl_entry = {
|
|
181
|
+
"default": default,
|
|
182
|
+
"type": acl_type,
|
|
183
|
+
"name": name,
|
|
184
|
+
"permissions": permissions,
|
|
185
|
+
}
|
|
186
|
+
return acl_entry
|
|
187
|
+
|
|
188
|
+
def _parse_acls(self):
|
|
189
|
+
"""
|
|
190
|
+
Parse ACL entries from the FACL output.
|
|
191
|
+
|
|
192
|
+
Example entries:
|
|
193
|
+
```
|
|
194
|
+
user::rwx
|
|
195
|
+
user:krauset:rwx
|
|
196
|
+
group::r-x
|
|
197
|
+
group:grp_hpc_collab002:r-x
|
|
198
|
+
mask::rwx
|
|
199
|
+
other::r-x
|
|
200
|
+
default:user::rwx
|
|
201
|
+
default:group::r-x
|
|
202
|
+
default:other::r-x
|
|
203
|
+
```
|
|
204
|
+
"""
|
|
205
|
+
for line in self.facl.splitlines():
|
|
206
|
+
if line.startswith("#") or line.strip() == "":
|
|
207
|
+
continue
|
|
208
|
+
acl_entry = self._parse_acl(line)
|
|
209
|
+
if acl_entry:
|
|
210
|
+
self.acls.append(acl_entry)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _permission_match(perm_key: str, perm_query: str, mode: str) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Check if the permission key matches the permission query for three different
|
|
216
|
+
modes:
|
|
217
|
+
|
|
218
|
+
Example: rwx vs rx
|
|
219
|
+
- 'exact': both must match exactly
|
|
220
|
+
- 'at_least': perm_key must have at least the permissions in perm_query
|
|
221
|
+
- 'at_most': perm_key must have at most the permissions in perm_query
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
perm_key (str): The permission key (e.g., 'rwx').
|
|
225
|
+
perm_query (str): The permission query (e.g., 'rx').
|
|
226
|
+
mode (str): The matching mode ('exact', 'at_least', 'at_most').
|
|
227
|
+
"""
|
|
228
|
+
if mode == "exact":
|
|
229
|
+
return perm_key == perm_query
|
|
230
|
+
elif mode == "at_least":
|
|
231
|
+
for char_query in perm_query:
|
|
232
|
+
if char_query != "-" and char_query not in perm_key:
|
|
233
|
+
return False
|
|
234
|
+
return True
|
|
235
|
+
elif mode == "at_most":
|
|
236
|
+
for char_key in perm_key:
|
|
237
|
+
if char_key != "-" and char_key not in perm_query:
|
|
238
|
+
return False
|
|
239
|
+
return True
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Invalid mode '{mode}'. Choose from 'exact', 'at_least', 'at_most'."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def _infer_groups(self, user: str) -> list:
|
|
246
|
+
"""
|
|
247
|
+
Infer groups for a given user using the `id -Gn` command.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
user (str): The username.
|
|
251
|
+
Returns:
|
|
252
|
+
list: List of groups the user belongs to.
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
["id", "-Gn", user], capture_output=True, text=True, check=True
|
|
257
|
+
)
|
|
258
|
+
groups = result.stdout.strip().split()
|
|
259
|
+
return groups
|
|
260
|
+
except subprocess.CalledProcessError as e:
|
|
261
|
+
self.logger.warning(f"Error retrieving groups for user {user}: {e}")
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
def has_permission(self, acl: str, mode: str = "at_least") -> bool:
|
|
265
|
+
"""
|
|
266
|
+
Check if a specific user or group has a certain permission.
|
|
267
|
+
Users are checked first, then groups, and finally 'other'.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
entity_type (str): 'user' or 'group'.
|
|
271
|
+
name (str): The name of the user or group.
|
|
272
|
+
permission (str): The permission to check ('r', 'w', or 'x').
|
|
273
|
+
"""
|
|
274
|
+
# parse acl
|
|
275
|
+
acl_entry = self._parse_acl(acl)
|
|
276
|
+
entity_type = acl_entry["type"]
|
|
277
|
+
name = acl_entry["name"]
|
|
278
|
+
permission = acl_entry["permissions"]
|
|
279
|
+
|
|
280
|
+
# check user
|
|
281
|
+
if entity_type in ["user"]:
|
|
282
|
+
for acl in self.acls:
|
|
283
|
+
if acl["default"]:
|
|
284
|
+
continue
|
|
285
|
+
if acl["type"] == "user" and acl["name"] == name:
|
|
286
|
+
return self._permission_match(acl["permissions"], permission, mode)
|
|
287
|
+
# check groups
|
|
288
|
+
if entity_type in ["user", "group"]:
|
|
289
|
+
if entity_type == "user":
|
|
290
|
+
groups = self._infer_groups(name)
|
|
291
|
+
else:
|
|
292
|
+
groups = [name]
|
|
293
|
+
for acl in self.acls:
|
|
294
|
+
if acl["default"]:
|
|
295
|
+
continue
|
|
296
|
+
if acl["type"] == "group" and acl["name"] in groups:
|
|
297
|
+
return self._permission_match(acl["permissions"], permission, mode)
|
|
298
|
+
|
|
299
|
+
# check other
|
|
300
|
+
if entity_type in ["user", "group", "other"]:
|
|
301
|
+
for acl in self.acls:
|
|
302
|
+
if acl["default"]:
|
|
303
|
+
continue
|
|
304
|
+
if acl["type"] == "other":
|
|
305
|
+
return self._permission_match(acl["permissions"], permission, mode)
|
|
306
|
+
return False
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyfacl"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Package to manage access control using POSIX ACLs"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "tobiaspk",email = "tobiaspk1@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = [
|
|
16
|
+
"pre-commit>=3.0.0",
|
|
17
|
+
"black>=23.0.0",
|
|
18
|
+
"flake8>=6.0.0",
|
|
19
|
+
"isort>=5.12.0",
|
|
20
|
+
"pytest>=7.0.0"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|
|
26
|
+
|
|
27
|
+
[tool.black]
|
|
28
|
+
line-length = 88
|
|
29
|
+
target-version = ['py312']
|
|
30
|
+
|
|
31
|
+
[tool.isort]
|
|
32
|
+
profile = "black"
|
|
33
|
+
line_length = 88
|
|
34
|
+
|
|
35
|
+
[tool.flake8]
|
|
36
|
+
max-line-length = 88
|
|
37
|
+
extend-ignore = ["E203"]
|