Implement hooking system for classes and methods. Cleanup of code

This commit is contained in:
Federico Luzzi 2024-05-03 17:32:45 -03:00
parent 87bb6302ff
commit 1bd9bd62c5
10 changed files with 542 additions and 1914 deletions

View File

@ -169,8 +169,8 @@ The `modify` method allows you to alter instances of a class at the time they ar
connapp.config.modify(modify_config) connapp.config.modify(modify_config)
``` ```
#### Implementing Hooks with `register_pre_hook` and `register_post_hook` #### Implementing Method Hooks
These methods allow you to define custom logic to be executed before (`register_pre_hook`) or after (`register_post_hook`) the main logic of a method. This is particularly useful for logging, auditing, preprocessing inputs, postprocessing outputs or adding functionalities. There are 2 methods that allows you to define custom logic to be executed before (`register_pre_hook`) or after (`register_post_hook`) the main logic of a method. This is particularly useful for logging, auditing, preprocessing inputs, postprocessing outputs or adding functionalities.
- **Usage**: Register hooks to methods to execute additional logic before or after the main method execution. - **Usage**: Register hooks to methods to execute additional logic before or after the main method execution.
- **Registration Methods Signature**: - **Registration Methods Signature**:
@ -305,7 +305,7 @@ import connpy
conf = connpy.configfile() conf = connpy.configfile()
organization = 'openai-org' organization = 'openai-org'
api_key = "openai-key" api_key = "openai-key"
myia = ai(conf, organization, api_key) myia = connpy.ai(conf, organization, api_key)
input = "go to router 1 and get me the full configuration" input = "go to router 1 and get me the full configuration"
result = myia.ask(input, dryrun = False) result = myia.ask(input, dryrun = False)
print(result) print(result)

View File

@ -149,8 +149,8 @@ The `modify` method allows you to alter instances of a class at the time they ar
connapp.config.modify(modify_config) connapp.config.modify(modify_config)
``` ```
#### Implementing Hooks with `register_pre_hook` and `register_post_hook` #### Implementing Method Hooks
These methods allow you to define custom logic to be executed before (`register_pre_hook`) or after (`register_post_hook`) the main logic of a method. This is particularly useful for logging, auditing, preprocessing inputs, postprocessing outputs or adding functionalities. There are 2 methods that allows you to define custom logic to be executed before (`register_pre_hook`) or after (`register_post_hook`) the main logic of a method. This is particularly useful for logging, auditing, preprocessing inputs, postprocessing outputs or adding functionalities.
- **Usage**: Register hooks to methods to execute additional logic before or after the main method execution. - **Usage**: Register hooks to methods to execute additional logic before or after the main method execution.
- **Registration Methods Signature**: - **Registration Methods Signature**:
@ -392,7 +392,7 @@ import connpy
conf = connpy.configfile() conf = connpy.configfile()
organization = 'openai-org' organization = 'openai-org'
api_key = "openai-key" api_key = "openai-key"
myia = ai(conf, organization, api_key) myia = connpy.ai(conf, organization, api_key)
input = "go to router 1 and get me the full configuration" input = "go to router 1 and get me the full configuration"
result = myia.ask(input, dryrun = False) result = myia.ask(input, dryrun = False)
print(result) print(result)
@ -413,5 +413,14 @@ __pdoc__ = {
'core': False, 'core': False,
'completion': False, 'completion': False,
'api': False, 'api': False,
'plugins': False 'plugins': False,
'core_plugins': False,
'hooks': False,
'connapp.start': False,
'ai.deferred_class_hooks': False,
'configfile.deferred_class_hooks': False,
'node.deferred_class_hooks': False,
'nodes.deferred_class_hooks': False,
'connapp': False,
'connapp.encrypt': True
} }

View File

@ -1,2 +1,2 @@
__version__ = "4.0.0b5" __version__ = "4.0.0"

View File

@ -177,17 +177,6 @@ Categorize the user's request based on the operation they want to perform on the
self.__prompt["confirmation_function"]["parameters"]["properties"]["response"]["type"] = "string" self.__prompt["confirmation_function"]["parameters"]["properties"]["response"]["type"] = "string"
self.__prompt["confirmation_function"]["parameters"]["required"] = ["result"] self.__prompt["confirmation_function"]["parameters"]["required"] = ["result"]
@MethodHook
def process_string(self, s):
if s.startswith('[') and s.endswith(']') and not (s.startswith("['") and s.endswith("']")) and not (s.startswith('["') and s.endswith('"]')):
# Extract the content inside square brackets and split by comma
content = s[1:-1].split(',')
# Add single quotes around each item and join them back together with commas
new_content = ', '.join(f"'{item.strip()}'" for item in content)
# Replace the old content with the new content
s = '[' + new_content + ']'
return s
@MethodHook @MethodHook
def _retry_function(self, function, max_retries, backoff_num, *args): def _retry_function(self, function, max_retries, backoff_num, *args):
#Retry openai requests #Retry openai requests

View File

@ -49,32 +49,45 @@ def _getcwd(words, option, folderonly=False):
return pathstrings return pathstrings
def _get_plugins(which, defaultdir): def _get_plugins(which, defaultdir):
enabled_files = [] # Path to core_plugins relative to this script
disabled_files = [] core_path = os.path.dirname(os.path.realpath(__file__)) + "/core_plugins"
all_files = []
all_plugins = {}
# Iterate over all files in the specified folder
for file in os.listdir(defaultdir + "/plugins"):
# Check if the file is a Python file
if file.endswith('.py'):
enabled_files.append(os.path.splitext(file)[0])
all_plugins[os.path.splitext(file)[0]] = os.path.join(defaultdir + "/plugins", file)
# Check if the file is a Python backup file
elif file.endswith('.py.bkp'):
disabled_files.append(os.path.splitext(os.path.splitext(file)[0])[0])
def get_plugins_from_directory(directory):
enabled_files = []
disabled_files = []
all_plugins = {}
# Iterate over all files in the specified folder
if os.path.exists(directory):
for file in os.listdir(directory):
# Check if the file is a Python file
if file.endswith('.py'):
enabled_files.append(os.path.splitext(file)[0])
all_plugins[os.path.splitext(file)[0]] = os.path.join(directory, file)
# Check if the file is a Python backup file
elif file.endswith('.py.bkp'):
disabled_files.append(os.path.splitext(os.path.splitext(file)[0])[0])
return enabled_files, disabled_files, all_plugins
# Get plugins from both directories
user_enabled, user_disabled, user_all_plugins = get_plugins_from_directory(defaultdir + "/plugins")
core_enabled, core_disabled, core_all_plugins = get_plugins_from_directory(core_path)
# Combine the results from user and core plugins
enabled_files = user_enabled
disabled_files = user_disabled
all_plugins = {**user_all_plugins, **core_all_plugins} # Merge dictionaries
# Return based on the command
if which == "--disable": if which == "--disable":
return enabled_files return enabled_files
elif which == "--enable": elif which == "--enable":
return disabled_files return disabled_files
elif which in ["--del", "--update"]: elif which in ["--del", "--update"]:
all_files.extend(enabled_files) all_files = enabled_files + disabled_files
all_files.extend(disabled_files)
return all_files return all_files
elif which == "all": elif which == "all":
return all_plugins return all_plugins
def main(): def main():
home = os.path.expanduser("~") home = os.path.expanduser("~")
defaultdir = home + '/.config/conn' defaultdir = home + '/.config/conn'
@ -144,7 +157,7 @@ def main():
if words[0] in ["--rm", "--del", "-r", "--mod", "--edit", "-e", "--show", "-s", "mv", "move", "cp", "copy"]: if words[0] in ["--rm", "--del", "-r", "--mod", "--edit", "-e", "--show", "-s", "mv", "move", "cp", "copy"]:
strings.extend(nodes) strings.extend(nodes)
if words[0] == "plugin": if words[0] == "plugin":
strings = ["--help", "--add", "--update", "--del", "--enable", "--disable"] strings = ["--help", "--add", "--update", "--del", "--enable", "--disable", "--list"]
if words[0] in ["run", "import", "export"]: if words[0] in ["run", "import", "export"]:
strings = ["--help"] strings = ["--help"]
if words[0] == "export": if words[0] == "export":

View File

@ -4,6 +4,7 @@ import json
import os import os
import re import re
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from pathlib import Path from pathlib import Path
from copy import deepcopy from copy import deepcopy
from .hooks import MethodHook, ClassHook from .hooks import MethodHook, ClassHook
@ -392,3 +393,32 @@ class configfile:
nodes.extend(layer3) nodes.extend(layer3)
return nodes return nodes
@MethodHook
def encrypt(self, password, keyfile=None):
'''
Encrypts password using RSA keyfile
### Parameters:
- password (str): Plaintext password to encrypt.
### Optional Parameters:
- keyfile (str): Path/file to keyfile. Default is config keyfile.
### Returns:
str: Encrypted password.
'''
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
encryptor = PKCS1_OAEP.new(publickey)
password = encryptor.encrypt(password.encode("utf-8"))
return str(password)

View File

@ -2,8 +2,6 @@
#Imports #Imports
import os import os
import re import re
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
import ast import ast
import argparse import argparse
import sys import sys
@ -1285,7 +1283,7 @@ class connapp:
passa = inquirer.prompt(passq) passa = inquirer.prompt(passq)
if passa == None: if passa == None:
return False return False
answer["password"] = self.encrypt(passa["password"]) answer["password"] = self.config.encrypt(passa["password"])
elif answer["password"] == "Profiles": elif answer["password"] == "Profiles":
passq = [(inquirer.Text("password", message="Set a @profile or a comma separated list of @profiles", validate=self._pass_validation))] passq = [(inquirer.Text("password", message="Set a @profile or a comma separated list of @profiles", validate=self._pass_validation))]
passa = inquirer.prompt(passq) passa = inquirer.prompt(passq)
@ -1355,7 +1353,7 @@ class connapp:
return False return False
if "password" in answer.keys(): if "password" in answer.keys():
if answer["password"] != "": if answer["password"] != "":
answer["password"] = self.encrypt(answer["password"]) answer["password"] = self.config.encrypt(answer["password"])
if "tags" in answer.keys() and answer["tags"]: if "tags" in answer.keys() and answer["tags"]:
answer["tags"] = ast.literal_eval(answer["tags"]) answer["tags"] = ast.literal_eval(answer["tags"])
result = {**answer, **profile} result = {**answer, **profile}
@ -1383,7 +1381,7 @@ class connapp:
if answer["password"] == "Local Password": if answer["password"] == "Local Password":
passq = [inquirer.Password("password", message="Set Password")] passq = [inquirer.Password("password", message="Set Password")]
passa = inquirer.prompt(passq) passa = inquirer.prompt(passq)
answer["password"] = self.encrypt(passa["password"]) answer["password"] = self.config.encrypt(passa["password"])
elif answer["password"] == "Profiles": elif answer["password"] == "Profiles":
passq = [(inquirer.Text("password", message="Set a @profile or a comma separated list of @profiles", validate=self._pass_validation))] passq = [(inquirer.Text("password", message="Set a @profile or a comma separated list of @profiles", validate=self._pass_validation))]
passa = inquirer.prompt(passq) passa = inquirer.prompt(passq)
@ -1550,31 +1548,3 @@ tasks:
output: null output: null
...''' ...'''
def encrypt(self, password, keyfile=None):
'''
Encrypts password using RSA keyfile
### Parameters:
- password (str): Plaintext password to encrypt.
### Optional Parameters:
- keyfile (str): Path/file to keyfile. Default is config keyfile.
### Returns:
str: Encrypted password.
'''
if keyfile is None:
keyfile = self.config.key
with open(keyfile) as f:
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
encryptor = PKCS1_OAEP.new(publickey)
password = encryptor.encrypt(password.encode("utf-8"))
return str(password)

View File

@ -35,21 +35,34 @@ class sync:
# The file token.json stores the user's access and refresh tokens. # The file token.json stores the user's access and refresh tokens.
if os.path.exists(self.token_file): if os.path.exists(self.token_file):
creds = Credentials.from_authorized_user_file(self.token_file, self.scopes) creds = Credentials.from_authorized_user_file(self.token_file, self.scopes)
# If there are no valid credentials available, let the user log in. try:
if not creds or not creds.valid: # If there are no valid credentials available, let the user log in.
if creds and creds.expired and creds.refresh_token: if not creds or not creds.valid:
creds.refresh(Request()) if creds and creds.expired and creds.refresh_token:
else: creds.refresh(Request())
flow = InstalledAppFlow.from_client_secrets_file( else:
self.google_client, self.scopes) flow = InstalledAppFlow.from_client_secrets_file(
creds = flow.run_local_server(port=0, access_type='offline') self.google_client, self.scopes)
creds = flow.run_local_server(port=0, access_type='offline')
# Save the credentials for the next run
# Save the credentials for the next run
with open(self.token_file, 'w') as token:
token.write(creds.to_json())
print("Logged in successfully.")
except RefreshError as e:
# If refresh fails, delete the invalid token file and start a new login flow
if os.path.exists(self.token_file):
os.remove(self.token_file)
print("Existing token was invalid and has been removed. Please log in again.")
flow = InstalledAppFlow.from_client_secrets_file(
self.google_client, self.scopes)
creds = flow.run_local_server(port=0, access_type='offline')
with open(self.token_file, 'w') as token: with open(self.token_file, 'w') as token:
token.write(creds.to_json()) token.write(creds.to_json())
print("Logged in successfully after re-authentication.")
print("Logged in successfully.")
def logout(self): def logout(self):
if os.path.exists(self.token_file): if os.path.exists(self.token_file):
@ -300,6 +313,8 @@ class sync:
if self.check_login_status() == True: if self.check_login_status() == True:
if not kwargs["result"]: if not kwargs["result"]:
self.compress_and_upload() self.compress_and_upload()
else:
print("Sync cannot be performed. Please check your login status.")
return kwargs["result"] return kwargs["result"]
def config_listener_pre(self, *args, **kwargs): def config_listener_pre(self, *args, **kwargs):

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#Imports #Imports
from functools import wraps, partial from functools import wraps, partial, update_wrapper
#functions and classes #functions and classes
@ -59,6 +59,7 @@ class ClassHook:
"""Decorator class to enable Class Modifying""" """Decorator class to enable Class Modifying"""
def __init__(self, cls): def __init__(self, cls):
self.cls = cls self.cls = cls
update_wrapper(self, cls, updated=()) # Update wrapper without changing underlying items
# Initialize deferred class hooks if they don't already exist # Initialize deferred class hooks if they don't already exist
if not hasattr(cls, 'deferred_class_hooks'): if not hasattr(cls, 'deferred_class_hooks'):
cls.deferred_class_hooks = [] cls.deferred_class_hooks = []

File diff suppressed because it is too large Load Diff