dprojectstools 0.0.3__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Marc Delòs Poch
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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.2
2
+ Name: dprojectstools
3
+ Version: 0.0.3
4
+ Summary: A Set of development tools
5
+ Author-email: Marc Delos <marcdp@dprojects.com>
6
+ Project-URL: Homepage, https://github.com/marcdp/dprojectstools
7
+ Project-URL: Issues, https://github.com/marcdp/dprojectstools/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+
15
+ # xmenu
16
+ A command line menu controller
@@ -0,0 +1,2 @@
1
+ # xmenu
2
+ A command line menu controller
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools]
6
+ package-dir = {"" = "src"} # Map the root to "src"
7
+
8
+ [tool.setuptools.packages.find]
9
+ where = ["src"]
10
+
11
+ [project]
12
+ name = "dprojectstools"
13
+ version = "0.0.3"
14
+ authors = [
15
+ { name="Marc Delos", email="marcdp@dprojects.com" },
16
+ ]
17
+ description = "A Set of development tools"
18
+ readme = "README.md"
19
+ requires-python = ">=3.8"
20
+ classifiers = [
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/marcdp/dprojectstools"
28
+ Issues = "https://github.com/marcdp/dprojectstools/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ # xmenu/__init__.py
2
+ #from .commands import command, example, CommandsManager
3
+ #from .secrets import SecretsManager
4
+ #from .crypto import aes_encrypt, aes_decrypt
@@ -0,0 +1,3 @@
1
+ # xmenu/__init__.py
2
+ from .commands import command, example, CommandsManager
3
+
@@ -0,0 +1,309 @@
1
+ import inspect
2
+ import types
3
+ from typing import List, get_type_hints
4
+ from dataclasses import dataclass
5
+
6
+ # global vars
7
+ order = 0
8
+ SEQUENCE_COLOR_RED = "\033[31m"
9
+ SEQUENCE_COLOR_GREEN = "\033[32m"
10
+ SEQUENCE_COLOR_GREEN_DARK = "\033[2;32m"
11
+ SEQUENCE_COLOR_GRAY_LIGHT = "\033[38;5;250m"
12
+ SEQUENCE_COLOR_GRAY_MEDIUM = "\033[38;5;244m"
13
+ SEQUENCE_COLOR_GRAY_DARK = "\033[38;5;238m"
14
+ SEQUENCE_RESET = "\033[0m"
15
+
16
+
17
+ # decorator (las funciones con este decorator, se catalogaran en el CommandsManager)
18
+ def command(title: str, index: int = 0):
19
+ def decorator(func):
20
+ global order
21
+ order += 1
22
+ aux = func.__name__ .split("_")
23
+ setattr(func, "title", title)
24
+ setattr(func, "order", order)
25
+ setattr(func, "index", index)
26
+ return func
27
+ return decorator
28
+
29
+
30
+ # decorator
31
+ def example(example: str):
32
+ def decorator(func):
33
+ setattr(func, "example", example)
34
+ return func
35
+ return decorator
36
+
37
+
38
+ # data classes
39
+ @dataclass
40
+ class CommandArgument:
41
+ name: str
42
+ title: str
43
+ required: bool
44
+ type: str
45
+ default: any
46
+
47
+ # data classes
48
+ @dataclass
49
+ class Command:
50
+ name: List[str]
51
+ title: str
52
+ arguments: List[CommandArgument]
53
+ instance: object
54
+ func: any
55
+ order:int
56
+ index: int
57
+
58
+
59
+ # Manager
60
+ class CommandsManager:
61
+
62
+ def __init__(self, title="", indent = 0):
63
+ # ctor
64
+ self._title = title
65
+ self._indent = indent
66
+ self._name = ""
67
+ self._argv = []
68
+ self._commands = []
69
+ self.exitCode = 0
70
+
71
+ # methods
72
+ def register(self, instance=None, prefix = ""):
73
+ if instance is None:
74
+ for name, obj in inspect.getmembers(__import__('__main__')):
75
+ if inspect.isfunction(obj) and name.startswith(prefix) and hasattr(obj, "title"):
76
+ self.registerFunction(None, obj, prefix)
77
+ else:
78
+ for name, obj in inspect.getmembers(type(instance), predicate=inspect.isfunction):
79
+ if inspect.isfunction(obj) and name.startswith(prefix) and hasattr(obj, "title"):
80
+ self.registerFunction(instance, obj, prefix)
81
+
82
+ def registerFunction(self, instance, func, prefix = ""):
83
+ # carga los Commands partiendo de las funciones cargadas en memoria, que empiezan con un determinado prefijo
84
+ func_name = func.__name__
85
+ command_name = func_name[len(prefix):].split("_")
86
+ command_title = getattr(func, "title")
87
+ command_order = getattr(func, "order")
88
+ command_index = getattr(func, "index")
89
+ command_arguments = []
90
+
91
+ for param_name, param in inspect.signature(func).parameters.items():
92
+ if param_name == "self":
93
+ continue
94
+ # argument name
95
+ command_argument_name = param_name
96
+ # argument title
97
+ command_argument_title = ""
98
+ if hasattr(param.annotation, "__metadata__"):
99
+ command_argument_title = param.annotation.__metadata__[0]
100
+ # argument title
101
+ command_argument_required = (param.default == inspect.Parameter.empty)
102
+ # argument type
103
+ command_argument_type = ""
104
+ if hasattr(param.annotation, "__metadata__"):
105
+ command_argument_type = param.annotation.__origin__.__name__
106
+ # argument default
107
+ command_argument_default = None
108
+ if param.default != inspect.Parameter.empty:
109
+ command_argument_default = param.default
110
+ # crea CommandArgument
111
+ command_argument = CommandArgument(command_argument_name, command_argument_title, command_argument_required, command_argument_type, command_argument_default)
112
+ command_arguments.append(command_argument)
113
+ # crea Comand
114
+ command = Command(name=command_name, title=command_title, arguments=command_arguments, instance=instance, func=func, order=command_order, index=command_index)
115
+ # añade a la lista de Commands
116
+ self._commands.append(command)
117
+
118
+ def sort(self):
119
+ # ordena los commands por order de declaracion de la funcion
120
+ self._commands.sort(key=lambda x: x.order)
121
+
122
+ # asigna indices
123
+ command_ant = None
124
+ index = 0
125
+ for command in self._commands:
126
+ if command.index != 0:
127
+ index = command.index
128
+ elif command_ant != None and command_ant.name[0] != command.name[0]:
129
+ index = int((index + 10) / 10) * 10
130
+ else:
131
+ index += 1
132
+ command.index = index
133
+ command_ant = command
134
+
135
+ self._commands.sort(key=lambda x: x.index)
136
+
137
+ #for command in self._commands:
138
+ # print(command.name, command.index)
139
+
140
+ def showMenu(self):
141
+ # sort
142
+ self.sort()
143
+ # title
144
+ if not self._title == "":
145
+ print(self._title)
146
+ print("*********")
147
+ # menu
148
+ indent = self._indent * " "
149
+ while True:
150
+ # show menu
151
+ print(indent + "Escoge una opción:")
152
+ print(indent + "==================")
153
+ command_ant = None
154
+ for command in self._commands:
155
+ if command_ant != None and command_ant.name[0] != command.name[0]:
156
+ print(indent + f" : ")
157
+ print(indent + f"{command.index:2} : {command.title}")
158
+ command_ant = command
159
+ print(indent + f" : ")
160
+ # read option
161
+ command_to_execute = None
162
+ while command_to_execute == None:
163
+ opcion = input(f" ? : ")
164
+ if opcion == "":
165
+ return 0
166
+ try:
167
+ opcion_index =int(opcion)
168
+ for command in self._commands:
169
+ if command.index == opcion_index:
170
+ command_to_execute = command
171
+ break
172
+ except:
173
+ pass
174
+ if command_to_execute == None:
175
+ print(indent + f"{SEQUENCE_COLOR_RED} índice no válido: {opcion}{SEQUENCE_RESET}")
176
+ # prepara argumentos
177
+ print()
178
+ exec_args = {}
179
+ errors = False
180
+ if len(command_to_execute.arguments) > 0:
181
+ for argument in command_to_execute.arguments:
182
+ argument_attributes = []
183
+ if argument.default != None:
184
+ argument_attributes.append(f"default es '{argument.default}'")
185
+ if argument.required:
186
+ argument_attributes.append(f"*")
187
+ argument_value = input(f" {argument.title}{"" if len(argument_attributes) == 0 else f" ({" ".join(argument_attributes)})"}: ").strip()
188
+ # valor por defecto
189
+ if not argument_value and argument.default != None:
190
+ argument_value = argument.default
191
+ # valida si es requerido
192
+ if argument.required:
193
+ if argument_value == "":
194
+ print(indent + f"{SEQUENCE_COLOR_RED} Error: argumento requerido: '{argument.title}'{SEQUENCE_RESET}")
195
+ errors = True
196
+ break
197
+ # convierte el tipo
198
+ try:
199
+ if argument.type == "int":
200
+ argument_value = int(argument_value)
201
+ elif argument.type == "float":
202
+ argument_value = float(argument_value)
203
+ elif argument.type == "List":
204
+ argument_value = argument_value.split(",")
205
+ except:
206
+ print(indent + f"{SEQUENCE_COLOR_RED} Error: el argumento '{argument.name}' no se ha podido convertir al tipo '{argument.type}': {argument_value}{SEQUENCE_RESET}")
207
+ errors = True
208
+ break
209
+ # set
210
+ exec_args[argument.name] = argument_value;
211
+ # exec
212
+ if not errors:
213
+ # invoke
214
+ if not command_to_execute.instance is None:
215
+ func_bounded = types.MethodType(command_to_execute.func, command_to_execute.instance)
216
+ result = func_bounded(**exec_args)
217
+ else:
218
+ result = command_to_execute.func(**exec_args)
219
+ # empty line
220
+ print()
221
+
222
+ def showHelp(self):
223
+ print(f"usage: {self._name} [<command> [<args>]]")
224
+ print("")
225
+ print("Commands:")
226
+ for command in self._commands:
227
+ line = " "
228
+ line += "Adasd"
229
+ args = []
230
+ for argument in command.arguments:
231
+ args.append(f"--{argument.name}")
232
+ args.append(f"{SEQUENCE_COLOR_GRAY_MEDIUM}<{argument.title}>{SEQUENCE_RESET}")
233
+ print(f" {" ".join(command.name):10} {" ".join(args)} {SEQUENCE_COLOR_GREEN_DARK}# {command.title}{SEQUENCE_RESET}")
234
+
235
+ def execute(self, argv):
236
+ # init
237
+ self._name = argv[0]
238
+ self._argv = argv
239
+ # ejecuta el comando que toque, segun self._argv
240
+ if len(self._argv)==1:
241
+ self.showMenu()
242
+ return
243
+ if len(self._argv)==2 and (self._argv[1] == "--help" or self._argv[1] == "-h"):
244
+ self.showHelp()
245
+ return
246
+ # busca el commando a ejecutar
247
+ command_to_execute = None
248
+ for command in self._commands:
249
+ if len(command.name) <= len(argv) - 1:
250
+ if command.name == argv[1:len(command.name)+1]:
251
+ command_to_execute = command
252
+ break
253
+ if command_to_execute:
254
+ break
255
+ # si no se ha encontrado, muestra el mesnaje de error
256
+ if command_to_execute == None:
257
+ print(f"error: no se ha encontrado el comando")
258
+ return -1
259
+ # ejecuta el comando
260
+ command_args = argv[len(command_to_execute.name)+1:]
261
+ command_args_dict = {}
262
+ command_args_errors = False
263
+ for i in range(0, len(command_args), 2):
264
+ if command_args[i].startswith('--'):
265
+ key = command_args[i][2:]
266
+ value = command_args[i + 1] if i + 1 < len(command_args) else None
267
+ command_args_dict[key] = value
268
+ # valida que no sobre ningun argumento
269
+ for key in command_args_dict.keys():
270
+ if not key in [argument.name for argument in command_to_execute.arguments]:
271
+ print(f"error: argumento inválido: --{key}")
272
+ command_args_errors = True
273
+ # aañade defaults
274
+ for argument in command.arguments:
275
+ if not argument.name in command_args_dict:
276
+ if argument.default != None:
277
+ command_args_dict[argument.name] = argument.default
278
+ # valida que no falte ningun argumento
279
+ for argument in command.arguments:
280
+ if not argument.name in command_args_dict:
281
+ print(f"error: argumento obligatorio: --{argument.name}")
282
+ command_args_errors = True
283
+ # valida que el tipo de argumentos sea correcto
284
+ if not command_args_errors:
285
+ for argument in command.arguments:
286
+ argument_value = command_args_dict[argument.name]
287
+ try:
288
+ if argument.type == "int":
289
+ argument_value = int(argument_value)
290
+ elif argument.type == "float":
291
+ argument_value = float(argument_value)
292
+ elif argument.type == "List":
293
+ argument_value = argument_value.split(",")
294
+ command_args_dict[argument.name] = argument_value
295
+ except:
296
+ print(f"{SEQUENCE_COLOR_RED}error: el argumento '{argument.name}' no se ha podido convertir al tipo '{argument.type}': {argument_value}{SEQUENCE_RESET}")
297
+ command_args_errors = True
298
+ break
299
+ # si hay errores
300
+ if command_args_errors:
301
+ return -1
302
+ # invoke
303
+ return command_to_execute.func(**command_args_dict)
304
+
305
+
306
+
307
+
308
+
309
+
@@ -0,0 +1 @@
1
+ from .aes import aes_encrypt, aes_decrypt, password_generate
@@ -0,0 +1,130 @@
1
+ from pathlib import Path
2
+ from cryptography.fernet import Fernet # pip install cryptography
3
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
4
+ from cryptography.hazmat.primitives.hashes import SHA256
5
+ from cryptography.hazmat.primitives import hashes
6
+ from cryptography.hazmat.primitives import padding
7
+ from cryptography.hazmat.backends import default_backend
8
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
9
+ import base64
10
+ import io
11
+ import os
12
+ import struct
13
+ import random
14
+ import hashlib
15
+ import random
16
+ import string
17
+
18
+ # utils
19
+ def password_generate(min=10, max=32):
20
+ length = random.randint(min, max)
21
+ characters = string.ascii_letters + string.digits + string.punctuation
22
+ password = ''.join(random.choice(characters) for i in range(length))
23
+ return password
24
+
25
+ # utils
26
+ def aes_encrypt(text: str, password: str):
27
+ iterations = 0
28
+ saltLength = 16
29
+ ivLength = 16
30
+ blockSize = 16
31
+ paddingMode = "PKCS7"
32
+ cipherMode = "CBC"
33
+ keySize = 32
34
+ encoding = "base64"
35
+ separator = ','
36
+ version = ""
37
+ fold = 76
38
+ # salt
39
+ salt = os.urandom(saltLength)
40
+ # iterations
41
+ iterations = int(random.uniform(50000, 50000))
42
+ # derive key
43
+ key = hashlib.pbkdf2_hmac(
44
+ hash_name='sha256', # HMAC-SHA256
45
+ password=password.encode(), # Password as bytes
46
+ salt=salt, # Salt as bytes
47
+ iterations=iterations, # Iteration count
48
+ dklen=keySize # Length of the derived key
49
+ )
50
+ # iv
51
+ iv = os.urandom(ivLength)
52
+ # ms
53
+ body_ms = io.BytesIO();
54
+ # write iv
55
+ body_ms.write(iv)
56
+ # write salt
57
+ body_ms.write(salt)
58
+ # write iterations
59
+ body_ms.write(bytearray(iterations.to_bytes(4, byteorder='little')))
60
+ # cipher
61
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
62
+ # encript
63
+ encryptor = cipher.encryptor()
64
+ # Pad the plaintext to a multiple of 16 bytes
65
+ padder = padding.PKCS7(blockSize * 8).padder()
66
+ padded_plaintext = padder.update(base64.b64encode(salt) + text.encode()) + padder.finalize()
67
+ #print(padded_plaintext)
68
+ # crypt
69
+ ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
70
+ body_ms.write(ciphertext)
71
+ # read
72
+ body_ms.seek(0)
73
+ body = body_ms.read()
74
+ body_ms.close()
75
+ # result
76
+ result = "aes:" + separator + base64.b64encode(body).decode()
77
+ # fold
78
+ result = "\n".join(
79
+ result[i:i + fold] for i in range(0, len(result), fold)
80
+ )
81
+ # return
82
+ return result
83
+
84
+ def aes_decrypt(text: str, password: str):
85
+ iterations = 0
86
+ saltLength = 16
87
+ ivLength = 16
88
+ blockSize = 16
89
+ paddingMode = "PKCS7"
90
+ cipherMode = "CBC"
91
+ keySize = 32
92
+ encoding = "base64"
93
+ separator = ','
94
+ version = ""
95
+ # header
96
+ header = text[:text.index(",")]
97
+ # body
98
+ body = text[text.index(",")+1:]
99
+ body_bytes = base64.b64decode(body)
100
+ stream = io.BytesIO(body_bytes)
101
+ # read iv
102
+ iv = stream.read(ivLength)
103
+ # read salt
104
+ salt = stream.read(saltLength)
105
+ # read iterations
106
+ iterations = struct.unpack('<I', stream.read(4))[0]
107
+ # derive key
108
+ key = hashlib.pbkdf2_hmac(
109
+ hash_name='sha256', # HMAC-SHA256
110
+ password=password.encode(), # Password as bytes
111
+ salt=salt, # Salt as bytes
112
+ iterations=iterations, # Iteration count
113
+ dklen=keySize # Length of the derived key
114
+ )
115
+ # cipher
116
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
117
+ # decryptor
118
+ decryptor = cipher.decryptor()
119
+ # decrypt
120
+ decrypted_result = decryptor.update(stream.read()) + decryptor.finalize()
121
+ # remove padding
122
+ unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
123
+ decrypted_result = unpadder.update(decrypted_result) + unpadder.finalize()
124
+ # decode
125
+ result = decrypted_result.decode()
126
+ # skip salt
127
+ result = result[int(saltLength*1.5):]
128
+ # return
129
+ return result
130
+
@@ -0,0 +1 @@
1
+ from .secrets import SecretsManager
@@ -0,0 +1,105 @@
1
+ from pathlib import Path
2
+ import os
3
+ import json
4
+ import keyring
5
+ from typing import Annotated
6
+ from ..commands import command
7
+ from ..crypto import aes_decrypt, aes_encrypt, password_generate
8
+
9
+ # consts
10
+ KEYRING_APP = "dprojectstools"
11
+
12
+ # class
13
+ class SecretsManager():
14
+
15
+
16
+ # vars
17
+ _password = None
18
+ _path = ""
19
+ _dict = {}
20
+
21
+
22
+ # ctr
23
+ def __init__(self, name, password = None):
24
+ folder = Path(os.path.join(Path.home(), ".secrets"))
25
+ folder.mkdir(parents=True, exist_ok=True)
26
+ self._path = Path(folder, name + ".json")
27
+ if not password is None:
28
+ if password.startswith("keyring:"):
29
+ username = password[password.index(":") + 1:]
30
+ if username == "":
31
+ username = os.getlogin()
32
+ password = keyring.get_password(KEYRING_APP, username)
33
+ print(username)
34
+ print(password)
35
+ if password is None:
36
+ password = password_generate()
37
+ keyring.set_password(KEYRING_APP, username, password)
38
+ self._password = password
39
+ self._path = Path(folder, name + ".json.aes")
40
+ self._load()
41
+
42
+
43
+ # methods
44
+ def get(self, name):
45
+ value = self._dict.get(name)
46
+ if value is None:
47
+ value = self.set(name, value)
48
+ return str(value)
49
+
50
+ def keys(self):
51
+ return self._dict.keys()
52
+
53
+ def set(self, name, value=None):
54
+ if value is None:
55
+ value = input("Enter secret '{0}' value: ".format(name))
56
+ self._dict[name] = str(value)
57
+ self._save()
58
+ return value
59
+
60
+ def delete(self, name):
61
+ del self._dict[name]
62
+ self._save()
63
+
64
+
65
+ # methods
66
+ def _load(self):
67
+ if os.path.isfile(self._path):
68
+ with open(self._path, "r") as file:
69
+ text = file.read()
70
+ if not self._password is None:
71
+ text = aes_decrypt(text, self._password);
72
+ self._dict = json.loads(text)
73
+ pass
74
+
75
+ def _save(self):
76
+ text = json.dumps(self._dict, indent=4)
77
+ if not self._password is None:
78
+ text = aes_encrypt(text, self._password);
79
+ with open(self._path, "w") as file:
80
+ file.write(text)
81
+
82
+
83
+ # ui method
84
+ @command("List secrets", index = 90)
85
+ def secrets_list(self):
86
+ for key in self.keys():
87
+ print("{0}: {1}".format(key, self.get(key)))
88
+ @command("Set secret")
89
+ def secrets_set(self,
90
+ name: Annotated[str, "Name"],
91
+ value: Annotated[str, "Value"]
92
+ ):
93
+ self.set(name, value)
94
+ @command("Get secret")
95
+ def secrets_get(self,
96
+ name: Annotated[str, "Name"]
97
+ ):
98
+ value = self.get(name)
99
+ print(value)
100
+ @command("Del secret")
101
+ def secrets_delete(self,
102
+ name: Annotated[str, "Name"]
103
+ ):
104
+ self.delete(name)
105
+
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.2
2
+ Name: dprojectstools
3
+ Version: 0.0.3
4
+ Summary: A Set of development tools
5
+ Author-email: Marc Delos <marcdp@dprojects.com>
6
+ Project-URL: Homepage, https://github.com/marcdp/dprojectstools
7
+ Project-URL: Issues, https://github.com/marcdp/dprojectstools/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+
15
+ # xmenu
16
+ A command line menu controller
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/dprojectstools/__init__.py
5
+ src/dprojectstools.egg-info/PKG-INFO
6
+ src/dprojectstools.egg-info/SOURCES.txt
7
+ src/dprojectstools.egg-info/dependency_links.txt
8
+ src/dprojectstools.egg-info/top_level.txt
9
+ src/dprojectstools/commands/__init__.py
10
+ src/dprojectstools/commands/commands.py
11
+ src/dprojectstools/crypto/__init__.py
12
+ src/dprojectstools/crypto/aes.py
13
+ src/dprojectstools/secrets/__init__.py
14
+ src/dprojectstools/secrets/secrets.py
@@ -0,0 +1 @@
1
+ dprojectstools