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,
|
||||
ticket_observer: TicketObserver = None,
|
||||
max_resolution: Optional[str] = None
|
||||
):
|
||||
) -> Result:
|
||||
|
||||
from core.controllers.ConnectionController import ConnectionController
|
||||
|
||||
|
|
@ -113,6 +113,7 @@ class ProfileController:
|
|||
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)
|
||||
return Result(valid=True)
|
||||
|
||||
# ============================================================================
|
||||
# SYSTEMWIDE
|
||||
|
|
@ -123,6 +124,7 @@ class ProfileController:
|
|||
if connection_result.valid:
|
||||
if profile_observer is not None:
|
||||
profile_observer.notify('enabled', profile)
|
||||
return Result(valid=True)
|
||||
else:
|
||||
logger.error(f"Couldn't enable the profile: {connection_result.error_type}")
|
||||
error_msg = connection_result.message
|
||||
|
|
|
|||
|
|
@ -56,7 +56,10 @@ class ThreadPlumbing:
|
|||
if not isinstance(result, Result):
|
||||
result = convert_to_result(result)
|
||||
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:
|
||||
error_msg = f"There was an error: {str(e)}"
|
||||
# notify(error_msg)
|
||||
|
|
|
|||
|
|
@ -8,17 +8,23 @@ from enum import Enum
|
|||
from pydantic import BaseModel, field_serializer, field_validator, ConfigDict
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from typing import Optional, Self
|
||||
from typing import Optional, Self, ClassVar
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class ConnectionChoice(str, Enum):
|
||||
TOR = "tor"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class Configuration(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
extra='ignore',
|
||||
exclude_none=True
|
||||
)
|
||||
|
||||
_instance: ClassVar[Optional[Self]] = None
|
||||
connection: Optional[ConnectionChoice] = None
|
||||
auto_sync_enabled: Optional[bool] = None
|
||||
endpoint_verification_enabled: Optional[bool] = False
|
||||
|
|
@ -28,10 +34,7 @@ class Configuration(BaseModel):
|
|||
did_sudo_setup: Optional[bool] = False
|
||||
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')
|
||||
@classmethod
|
||||
|
|
@ -47,9 +50,12 @@ class Configuration(BaseModel):
|
|||
return value.isoformat().replace('+00:00', 'Z') # +00:00 → Z for JSON
|
||||
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:
|
||||
with open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r') as f:
|
||||
config_file_contents = f.read()
|
||||
|
|
@ -61,18 +67,41 @@ class Configuration(BaseModel):
|
|||
except ValueError:
|
||||
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: Self):
|
||||
def save(self) -> None:
|
||||
# model_dump_json reflects the current state of this specific instance
|
||||
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'
|
||||
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():
|
||||
"""
|
||||
Returns it as a dict, not as a model.
|
||||
"""
|
||||
try:
|
||||
config_file_contents = open(f'{Constants.HV_CONFIG_HOME}/config.json', 'r').read()
|
||||
except FileNotFoundError:
|
||||
|
|
@ -84,17 +113,3 @@ def read_config():
|
|||
logger.error(f"[CONFIG] Can't load JSON config")
|
||||
|
||||
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"
|
||||
INVALID_API_REPLY = "invalid_api_reply"
|
||||
ENCRYPTION = "encryption"
|
||||
ENDPOINT_VERIFICATION = "endpoint_verification"
|
||||
LEAK_ISSUE = "leak_issue"
|
||||
NEED_SYNC = "need_sync"
|
||||
WRONG_LOCATION = "wrong_location"
|
||||
|
|
@ -42,6 +43,8 @@ class ResultError(Enum):
|
|||
UNPAID = "unpaid"
|
||||
MTU = "mtu"
|
||||
ACTIVE_BILLING = "active_billing"
|
||||
STATE_CONFLICT = "state_conflict"
|
||||
TOR_NEEDED = "tor_needed"
|
||||
NAVIGATE_TO = "navigate_to"
|
||||
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