The Plumbing now allows for the service to provide it's own function_choice when it returns a result. Separetly, a new type_checker wrapper is now available for use. And finally, the config model now passes around the same instance as a singleton to avoid new file operations to just read
This commit is contained in:
parent
7ba8e1e6d3
commit
81002cd76d
6 changed files with 110 additions and 30 deletions
|
|
@ -61,7 +61,7 @@ class ProfileController:
|
||||||
connection_observer: ConnectionObserver = None,
|
connection_observer: ConnectionObserver = None,
|
||||||
ticket_observer: TicketObserver = None,
|
ticket_observer: TicketObserver = None,
|
||||||
max_resolution: Optional[str] = None
|
max_resolution: Optional[str] = None
|
||||||
):
|
) -> Result:
|
||||||
|
|
||||||
from core.controllers.ConnectionController import ConnectionController
|
from core.controllers.ConnectionController import ConnectionController
|
||||||
|
|
||||||
|
|
@ -113,6 +113,7 @@ class ProfileController:
|
||||||
profile_observer.notify('enabled', profile)
|
profile_observer.notify('enabled', profile)
|
||||||
|
|
||||||
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
|
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
|
||||||
|
return Result(valid=True)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SYSTEMWIDE
|
# SYSTEMWIDE
|
||||||
|
|
@ -123,6 +124,7 @@ class ProfileController:
|
||||||
if connection_result.valid:
|
if connection_result.valid:
|
||||||
if profile_observer is not None:
|
if profile_observer is not None:
|
||||||
profile_observer.notify('enabled', profile)
|
profile_observer.notify('enabled', profile)
|
||||||
|
return Result(valid=True)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
|
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
|
||||||
error_msg = connection_result.message
|
error_msg = connection_result.message
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,10 @@ class ThreadPlumbing:
|
||||||
if not isinstance(result, Result):
|
if not isinstance(result, Result):
|
||||||
result = convert_to_result(result)
|
result = convert_to_result(result)
|
||||||
if not result.valid:
|
if not result.valid:
|
||||||
result.function_choice = resolve_prescription(result, self.ENUM_MAP, *self.args, **self.kwargs)
|
if not result.function_choice:
|
||||||
|
result.function_choice = resolve_prescription(result, self.ENUM_MAP, *self.args, **self.kwargs)
|
||||||
|
else:
|
||||||
|
logger.info("Using the pre-existing function choice for this error.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"There was an error: {str(e)}"
|
error_msg = f"There was an error: {str(e)}"
|
||||||
# notify(error_msg)
|
# notify(error_msg)
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,23 @@ from enum import Enum
|
||||||
from pydantic import BaseModel, field_serializer, field_validator, ConfigDict
|
from pydantic import BaseModel, field_serializer, field_validator, ConfigDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from typing import Optional, Self
|
from typing import Optional, Self, ClassVar
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class ConnectionChoice(str, Enum):
|
class ConnectionChoice(str, Enum):
|
||||||
TOR = "tor"
|
TOR = "tor"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
||||||
|
|
||||||
class Configuration(BaseModel):
|
class Configuration(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='ignore',
|
||||||
|
exclude_none=True
|
||||||
|
)
|
||||||
|
|
||||||
|
_instance: ClassVar[Optional[Self]] = None
|
||||||
connection: Optional[ConnectionChoice] = None
|
connection: Optional[ConnectionChoice] = None
|
||||||
auto_sync_enabled: Optional[bool] = None
|
auto_sync_enabled: Optional[bool] = None
|
||||||
endpoint_verification_enabled: Optional[bool] = False
|
endpoint_verification_enabled: Optional[bool] = False
|
||||||
|
|
@ -28,10 +34,7 @@ class Configuration(BaseModel):
|
||||||
did_sudo_setup: Optional[bool] = False
|
did_sudo_setup: Optional[bool] = False
|
||||||
singbox: Optional[str] = None
|
singbox: Optional[str] = None
|
||||||
|
|
||||||
model_config = ConfigDict(
|
|
||||||
extra='ignore', # Ignore unknown fields in JSON
|
|
||||||
exclude_none=True # Don't serialize None values
|
|
||||||
)
|
|
||||||
|
|
||||||
@field_validator('last_synced_at', mode='before')
|
@field_validator('last_synced_at', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -47,9 +50,12 @@ class Configuration(BaseModel):
|
||||||
return value.isoformat().replace('+00:00', 'Z') # +00:00 → Z for JSON
|
return value.isoformat().replace('+00:00', 'Z') # +00:00 → Z for JSON
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls) -> Optional[Self]:
|
||||||
|
# Return the cached instance if it exists
|
||||||
|
if cls._instance is not None:
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get():
|
|
||||||
try:
|
try:
|
||||||
with open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r') as f:
|
with open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r') as f:
|
||||||
config_file_contents = f.read()
|
config_file_contents = f.read()
|
||||||
|
|
@ -61,18 +67,41 @@ class Configuration(BaseModel):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return Configuration(**configuration_dict) # Pydantic validates on init
|
# Create the instance and cache it before returning
|
||||||
|
cls._instance = cls(**configuration_dict)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
def save(self: Self):
|
# model_dump_json reflects the current state of this specific instance
|
||||||
config_file_contents = f'{self.model_dump_json(indent=4)}\n'
|
config_file_contents = f'{self.model_dump_json(indent=4)}\n'
|
||||||
os.makedirs(Constants.HV_CONFIG_HOME, exist_ok=True)
|
os.makedirs(Constants.HOME, exist_ok=True)
|
||||||
|
|
||||||
config_file_path = f'{Constants.HV_CONFIG_HOME}/config.json'
|
config_file_path = f'{Constants.HV_CONFIG_HOME}/config.json'
|
||||||
write_atomically(config_file_path, config_file_contents)
|
write_atomically(config_file_path, config_file_contents)
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting(looking_for: str):
|
||||||
|
# Use the singleton getter
|
||||||
|
config = Configuration.get()
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
logger.error(f"[CONFIG] Can't load the entire config")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# getattr because Pydantic models are objects, not dicts
|
||||||
|
result = getattr(config, looking_for, None)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
logger.error(f"[CONFIG] What you want isn't in the config: {looking_for}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def read_config():
|
def read_config():
|
||||||
|
"""
|
||||||
|
Returns it as a dict, not as a model.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
|
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|
@ -84,17 +113,3 @@ def read_config():
|
||||||
logger.error(f"[CONFIG] Can't load JSON config")
|
logger.error(f"[CONFIG] Can't load JSON config")
|
||||||
|
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
def get_setting(looking_for):
|
|
||||||
config = read_config()
|
|
||||||
if not config:
|
|
||||||
logger.error(f"[CONFIG] Can't load the entire config")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if looking_for not in config:
|
|
||||||
logger.error(f"[CONFIG] What you want isn't in the config")
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = config[looking_for]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class ResultError(Enum):
|
||||||
TIMEOUT = "timeout"
|
TIMEOUT = "timeout"
|
||||||
INVALID_API_REPLY = "invalid_api_reply"
|
INVALID_API_REPLY = "invalid_api_reply"
|
||||||
ENCRYPTION = "encryption"
|
ENCRYPTION = "encryption"
|
||||||
|
ENDPOINT_VERIFICATION = "endpoint_verification"
|
||||||
LEAK_ISSUE = "leak_issue"
|
LEAK_ISSUE = "leak_issue"
|
||||||
NEED_SYNC = "need_sync"
|
NEED_SYNC = "need_sync"
|
||||||
WRONG_LOCATION = "wrong_location"
|
WRONG_LOCATION = "wrong_location"
|
||||||
|
|
@ -42,6 +43,8 @@ class ResultError(Enum):
|
||||||
UNPAID = "unpaid"
|
UNPAID = "unpaid"
|
||||||
MTU = "mtu"
|
MTU = "mtu"
|
||||||
ACTIVE_BILLING = "active_billing"
|
ACTIVE_BILLING = "active_billing"
|
||||||
|
STATE_CONFLICT = "state_conflict"
|
||||||
|
TOR_NEEDED = "tor_needed"
|
||||||
NAVIGATE_TO = "navigate_to"
|
NAVIGATE_TO = "navigate_to"
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
|
||||||
14
core/utils/confirm_preqs.py
Normal file
14
core/utils/confirm_preqs.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def confirm_prereqs(PRE_REQS: list, allow_false: Optional[bool] = False) -> Result:
|
||||||
|
for each_prereq in PRE_REQS:
|
||||||
|
if allow_false:
|
||||||
|
if each_prereq == None:
|
||||||
|
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}")
|
||||||
|
else:
|
||||||
|
if not each_prereq:
|
||||||
|
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}")
|
||||||
|
return Result(valid=True)
|
||||||
|
|
||||||
43
core/utils/type_checker.py
Normal file
43
core/utils/type_checker.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from core.models.Result import Result
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
def type_checker(func):
|
||||||
|
"""
|
||||||
|
Type check wrapper.
|
||||||
|
This type checks individual functions it's put on, and returns a Result object on failure.
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# 1. Introspect the wrapped function
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Bind the provided args/kwargs to the function's signature
|
||||||
|
bound_args = sig.bind(*args, **kwargs)
|
||||||
|
bound_args.apply_defaults()
|
||||||
|
except TypeError as e:
|
||||||
|
# This handles cases where the wrong number of arguments are passed
|
||||||
|
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=str(e))
|
||||||
|
|
||||||
|
# 2. Perform type validation
|
||||||
|
for name, value in bound_args.arguments.items():
|
||||||
|
expected_type = sig.parameters[name].annotation
|
||||||
|
|
||||||
|
# Skip parameters that don't have a type hint
|
||||||
|
if expected_type is inspect.Parameter.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(value, expected_type):
|
||||||
|
return Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.INVALID_INPUT,
|
||||||
|
message=f"Argument '{name}' must be {expected_type.__name__}, got type {type(value).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. If all checks pass, call the original function
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
Loading…
Reference in a new issue