Compare commits

..

5 commits

14 changed files with 231 additions and 91 deletions

View file

@ -1 +0,0 @@
this is june 13 version

View file

@ -1,3 +1,6 @@
from core.errors.exceptions import *
from core.errors.logger import logger
from collections.abc import Callable from collections.abc import Callable
from core.Constants import Constants from core.Constants import Constants
from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ConfigurationController import ConfigurationController
@ -58,6 +61,8 @@ class ConnectionController:
connection = profile.connection connection = profile.connection
# needs_proxy_configuration comes from the child connection models
# for the system it returns false blindly. for the session is checks if the connection type is masked.
if connection.needs_proxy_configuration() and not profile.has_proxy_configuration(): if connection.needs_proxy_configuration() and not profile.has_proxy_configuration():
if profile.has_subscription(): if profile.has_subscription():
@ -75,6 +80,8 @@ class ConnectionController:
else: else:
raise MissingSubscriptionError() raise MissingSubscriptionError()
# The needs_wireguard_configuration comes from the connection model, and can be transitioned to get it directly from the object's data
# The has_wireguard_configuration comes from each polymorph object doing an os check on if the wg config exists
if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration(): if connection.needs_wireguard_configuration() and not profile.has_wireguard_configuration():
if profile.has_subscription(): if profile.has_subscription():
@ -161,6 +168,7 @@ class ConnectionController:
port_number = None port_number = None
proxy_port_number = None proxy_port_number = None
# this is a check from SessionConnection of if there's a systemwide with mask
if profile.connection.is_unprotected(): if profile.connection.is_unprotected():
if not ConnectionController.system_uses_wireguard_interface(): if not ConnectionController.system_uses_wireguard_interface():
@ -246,42 +254,11 @@ class ConnectionController:
pass pass
if profile.connection.needs_operator_proxy(): if profile.connection.needs_operator_proxy():
operator_proxy_session = profile.get_operator_proxy_session() ok = ConnectionController.establish_encrypted_proxy_connection(
protocol = profile.connection.get_protocol() profile, socks5_port=1080, observer=connection_observer
if protocol == 'vless':
from core.controllers.encrypted_proxy.VlessController import VlessController
from core.services.encrypted_proxy.vless_service import parse_vless_link
import socket
server_ip = operator_proxy_session.server_ip
if server_ip is None:
vless = parse_vless_link(operator_proxy_session.links[0])
server_ip = socket.gethostbyname(vless['host'])
ok = VlessController(1080).enable(
operator_proxy_session.links[0],
operator_proxy_session.username,
server_ip,
connection_observer
) )
elif protocol == 'hysteria2':
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
import socket
server_ip = operator_proxy_session.server_ip
if server_ip is None:
server_ip = socket.gethostbyname(operator_proxy_session.operator_hysteria2_host)
ok = HysteriaController(1080).enable(
operator_proxy_session.username,
operator_proxy_session.password,
operator_proxy_session.operator_hysteria2_host,
server_ip,
connection_observer
)
else:
ok = False
if not ok: if not ok:
raise ConnectionError('The connection could not be established.') raise ConnectionError('The connection could not be established.')
token = SystemStateController.create(profile.id) token = SystemStateController.create(profile.id)
if connection_observer is not None: if connection_observer is not None:
connection_observer.notify('connected_token', {'session_token': token}) connection_observer.notify('connected_token', {'session_token': token})
@ -317,6 +294,40 @@ class ConnectionController:
ConnectionController.terminate_tor_connection() ConnectionController.terminate_tor_connection()
time.sleep(1.0) time.sleep(1.0)
@staticmethod
def establish_encrypted_proxy_connection(profile, socks5_port: int = 1080, observer=None) -> bool:
"""Shared encrypted proxy connection logic for CLI and GUI."""
import socket
operator_proxy_session = profile.get_operator_proxy_session()
protocol = profile.connection.code
if protocol == 'vless':
from core.controllers.encrypted_proxy.VlessController import VlessController
from core.services.encrypted_proxy.vless_service import parse_vless_link
server_ip = operator_proxy_session.server_ip
if server_ip is None:
vless = parse_vless_link(operator_proxy_session.links[0])
server_ip = socket.gethostbyname(vless['host'])
return VlessController(socks5_port).enable(
operator_proxy_session.links[0],
operator_proxy_session.username,
server_ip,
observer
)
elif protocol == 'hysteria2':
from core.controllers.encrypted_proxy.HysteriaController import HysteriaController
server_ip = operator_proxy_session.server_ip
if server_ip is None:
server_ip = socket.gethostbyname(operator_proxy_session.operator_hysteria2_host)
return HysteriaController(socks5_port).enable(
operator_proxy_session.username,
operator_proxy_session.password,
operator_proxy_session.operator_hysteria2_host,
server_ip,
observer
)
return False
@staticmethod @staticmethod
def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None): def establish_tor_connection(connection_observer: Optional[ConnectionObserver] = None):
@ -336,9 +347,13 @@ class ConnectionController:
@staticmethod @staticmethod
def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None): def establish_tor_session_connection(port_number: int, connection_observer: Optional[ConnectionObserver] = None):
try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME) tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer) tor_module.create_session(port_number, connection_observer)
except TorServiceInitializationError as e:
logger.error(f"TorServiceInitializationError. Tor Can't Start: {e}")
except Exception as e:
logger.error(f"Tor Can't Start: {e}")
@staticmethod @staticmethod
def terminate_tor_session_connection(port_number: int): def terminate_tor_session_connection(port_number: int):

View file

@ -9,10 +9,23 @@ class SubscriptionPlanController:
@staticmethod @staticmethod
def get(connection: Union[SessionConnection, SystemConnection], duration: int): def get(connection: Union[SessionConnection, SystemConnection], duration: int):
"""
Called by:
GUI in worker.py
Purpose:
confirm the subscription's length is valid
"""
return SubscriptionPlan.find(connection, duration) return SubscriptionPlan.find(connection, duration)
@staticmethod @staticmethod
def get_all(connection: Optional[Union[SessionConnection, SystemConnection]] = None): def get_all(connection: Optional[Union[SessionConnection, SystemConnection]] = None):
"""
Not used. Good candidate to be cut.
GUI's create_interface_elements
inside duration selection page actually has hardcoded amounts
"""
return SubscriptionPlan.all(connection) return SubscriptionPlan.all(connection)
@staticmethod @staticmethod

View file

@ -1,3 +1,7 @@
class TorServiceInitializationError(Exception):
pass
class ApplicationError(Exception): class ApplicationError(Exception):
pass pass

View file

@ -1,18 +1,14 @@
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses_json import dataclass_json from dataclasses_json import dataclass_json
@dataclass_json @dataclass_json
@dataclass @dataclass
class BaseConnection: class BaseConnection:
code: str code: str
# called by: connection controller for basic type checks on wireguard code
def needs_wireguard_configuration(self): def needs_wireguard_configuration(self):
return self.code == 'wireguard' return self.code == 'wireguard'
def is_session_connection(self): def is_session_connection(self):
return type(self).__name__ == 'SessionConnection' return type(self).__name__ == 'SessionConnection'
def is_system_connection(self): def is_system_connection(self):
return type(self).__name__ == 'SystemConnection' return type(self).__name__ == 'SystemConnection'
def needs_operator_proxy(self): def needs_operator_proxy(self):

View file

@ -53,7 +53,7 @@ class SubscriptionPlan(Model):
if connection.code == 'wireguard': if connection.code == 'wireguard':
features_wireguard = True features_wireguard = True
if connection.code == 'operator': if connection.code in ('operator', 'vless', 'hysteria2'):
features_proxy = True features_proxy = True
Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition) Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition)
@ -79,7 +79,7 @@ class SubscriptionPlan(Model):
if connection.code == 'wireguard': if connection.code == 'wireguard':
features_wireguard = True features_wireguard = True
if connection.code == 'operator': if connection.code in ('operator', 'vless', 'hysteria2'):
features_proxy = True features_proxy = True
return Model._query_all('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ?', SubscriptionPlan.factory, [features_proxy, features_wireguard]) return Model._query_all('SELECT * FROM subscription_plans WHERE features_proxy = ? AND features_wireguard = ?', SubscriptionPlan.factory, [features_proxy, features_wireguard])

View file

@ -27,11 +27,10 @@ def extract_from_nested(obj, yaml_keys, default=None):
This function removes unnecessary nesting, This function removes unnecessary nesting,
by comparing it to a pre-made YAML mapping by comparing it to a pre-made YAML mapping
""" """
def denormalize(data, which_yaml_file): def denormalize(data, mapping_file):
final_results = [] final_results = []
# prep the mapping config: # prep the mapping config:
mapping_file = f"assets/yaml_mappings/{which_yaml_file}"
with open(mapping_file) as f: with open(mapping_file) as f:
mapping = yaml.safe_load(f) mapping = yaml.safe_load(f)

View file

@ -1,6 +1,6 @@
from core.errors.logger import logger from core.errors.logger import logger
from core.models.orm_models.Base import BaseModel from core.models.orm_models.Base import BaseModel
from sqlalchemy import create_engine from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
import os import os
from pathlib import Path from pathlib import Path
@ -18,9 +18,15 @@ def get_path():
"""Returns XDG Base Directory path or ~/.local/share/hydra-veil""" """Returns XDG Base Directory path or ~/.local/share/hydra-veil"""
xdg_data = os.getenv("XDG_DATA_HOME") xdg_data = os.getenv("XDG_DATA_HOME")
if xdg_data: if xdg_data:
return Path(xdg_data) / "my-app" return Path(xdg_data) / "hydra-veil"
return Path.home() / ".local" / "share" / "hydra-veil" return Path.home() / ".local" / "share" / "hydra-veil"
def does_it_exist(filepath):
if filepath.exists():
return True
else:
return False
# ============================================================================ # ============================================================================
# GLOBAL STATE # GLOBAL STATE
# ============================================================================ # ============================================================================
@ -80,33 +86,42 @@ def create_ONLY_db_version_table():
raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.") raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
from core.models.orm_models.DatabaseVersion import database_version from core.models.orm_models.DatabaseVersion import database_version
database_version.create(engine, checkfirst=True) try:
# DatabaseVersion.__table__.create(engine, checkfirst=True) database_version.create(engine)
except:
logger.info("[DB MANAGEMENT] database_version already exists, but was attempted to be made again by calling create_ONLY_db_version_table.")
# if using without checking or try except blocks:
# database_version.create(engine, checkfirst=True)
def does_db_version_table_exist():
if engine is None:
raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
inspector = inspect(engine)
table_exists = inspector.has_table("database_version")
return table_exists
def create_ALL_tables(): def create_ALL_tables():
"""Create all tables from BaseModel.metadata using the global engine.""" """Create all tables from BaseModel.metadata using the global engine."""
print("starting create_ALL_tables")
if engine is None: if engine is None:
raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.") raise RuntimeError("Engine not initialized. Call _reinitialize_engine_and_session() first.")
print("passed engine test")
from core.models.orm_models.Location import Location from core.models.orm_models.Location import Location
from core.models.orm_models.Operator import Operator from core.models.orm_models.Operator import Operator
from core.models.orm_models.CachedSync import CachedSync from core.models.orm_models.CachedSync import CachedSync
from core.models.orm_models.EncryptedProxy import EncryptedProxy from core.models.orm_models.EncryptedProxy import EncryptedProxy
from core.models.SubscriptionPlan import SubscriptionPlan from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.session.ApplicationVersion import ApplicationVersion from core.models.session.ApplicationVersion import ApplicationVersion
try: try:
print("trying this")
BaseModel.metadata.create_all(engine, checkfirst=True) BaseModel.metadata.create_all(engine, checkfirst=True)
print("Returning True for Creating all Tables..") logger.info("[DB MANAGEMENT] All Tables have been successfully created.")
return True return True
except: except:
print("couldn't make all tables") logger.error("[DB MANAGEMENT] Fatal Error with creating all tables in the create_ALL_tables function of session management.")
return False return False

View file

@ -9,6 +9,8 @@ class SessionConnection(BaseConnection):
if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'): if self.code not in ('system', 'tor', 'wireguard', 'vless', 'hysteria2'):
raise ValueError('Invalid connection code.') raise ValueError('Invalid connection code.')
# called by connection controller
# doesn't even make sense, it's checking if the Session is code system. this will always be false.
def is_unprotected(self): def is_unprotected(self):
return self.code == 'system' and self.masked is False return self.code == 'system' and self.masked is False

View file

@ -8,7 +8,7 @@ class SystemConnection(BaseConnection):
protocol: Optional[str] = field(default=None) protocol: Optional[str] = field(default=None)
def __post_init__(self): def __post_init__(self):
if self.code not in ('wireguard', 'operator'): if self.code not in ('wireguard', 'operator', 'vless', 'hysteria2'):
raise ValueError('Invalid connection code.') raise ValueError('Invalid connection code.')
@staticmethod @staticmethod
@ -16,7 +16,7 @@ class SystemConnection(BaseConnection):
return False return False
def needs_operator_proxy(self): def needs_operator_proxy(self):
return self.code == 'operator' return self.code in ('operator', 'vless', 'hysteria2')
def get_protocol(self): def get_protocol(self):
return self.protocol return self.protocol if self.protocol else self.code

View file

@ -16,54 +16,98 @@ from core.errors.logger import logger
import json, os import json, os
from typing import Any from typing import Any
from core.Constants import Constants from core.Constants import Constants
import time
def tor_get_request(custom_url: str, connection_observer: ConnectionObserver) -> dict: def tor_get_request(custom_url: str, connection_observer: ConnectionObserver) -> dict:
import requests import requests
# ============================================================================
# INITIALIZE TOR
# ============================================================================
port_number = ConnectionService.get_random_available_port_number() port_number = ConnectionService.get_random_available_port_number()
logger.debug(f"Using Tor on port number {port_number}") logger.debug(f"[USE TOR SERVICE] Using Tor on port number {port_number}")
try: try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME) tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer) tor_module.create_session(port_number, connection_observer)
logger.debug(f"[USE TOR SERVICE] Tor Started a Session")
except TorServiceInitializationError as e:
error_msg = f"[USE TOR SERVICE] Tor failed to start: {e}"
tor_module.destroy_session(port_number)
logger.error(error_msg, exc_info=True)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except Exception as e:
logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}")
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
proxies = { proxies = {
"http": f"socks5h://127.0.0.1:{port_number}", "http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}", "https": f"socks5h://127.0.0.1:{port_number}",
} }
logger.debug(f"Doing Request through Tor via port {port_number}") # ============================================================================
# USE TOR AS PROXY
# ============================================================================
try:
logger.debug(f"[TOR GET REQUEST] Using Tor on port number {port_number}")
response = requests.get(custom_url, proxies=proxies, timeout=5) response = requests.get(custom_url, proxies=proxies, timeout=5)
logger.debug("Request through Tor successful.") logger.debug("[TOR GET REQUEST] Request through Tor Successful!")
# if it crashes from the above error check, then it won't destroy the proxy, # if it crashes from an error in trying to get a JSON out of it, then we'd skip destroying the Tor session,
# so we'll destroy it now first,
logger.debug("[TOR GET REQUEST] DESTROY SESSION!")
tor_module.destroy_session(port_number) tor_module.destroy_session(port_number)
# return response # Now that it's safe, get a JSON out of it, and return the response
dictonary_of_response = response.json() dictonary_of_response = response.json()
logger.debug(f"[TOR GET REQUEST] Got Data out of the reply. {dictonary_of_response}")
final_reply = {"valid": True, "data": dictonary_of_response} final_reply = {"valid": True, "data": dictonary_of_response}
return final_reply return final_reply
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem( problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer, port_number, proxies custom_url, connection_observer
) )
return problem_results return problem_results
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
logger.debug("Connection timed out") logger.debug("Connection timed out")
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem( problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer, port_number custom_url, connection_observer
) )
return problem_results return problem_results
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
logger.debug(f"HTTP error: {response.status_code}") logger.debug(f"HTTP error: {response.status_code}")
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem( problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer, port_number custom_url, connection_observer
)
return problem_results
except requests.exceptions.ProxyError:
error_msg = "[USE TOR SERVICE] Requests Proxy Error"
tor_module.destroy_session(port_number)
logger.error(error_msg, exc_info=True)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except Exception as e:
logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}")
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
) )
return problem_results return problem_results
@ -80,41 +124,63 @@ def tor_post_request(
"status": "unable_to_get" "status": "unable_to_get"
} # incase this goes to the except block, it's "defined" } # incase this goes to the except block, it's "defined"
# ============================================================================
# INITIALIZE TOR
# ============================================================================
port_number = ConnectionService.get_random_available_port_number() port_number = ConnectionService.get_random_available_port_number()
logger.debug(f"Using Tor on port number {port_number}") logger.debug(f"Using Tor on port number {port_number}")
try: try:
tor_module = TorModule(Constants.HV_TOR_STATE_HOME) tor_module = TorModule(Constants.HV_TOR_STATE_HOME)
tor_module.create_session(port_number, connection_observer) tor_module.create_session(port_number, connection_observer)
except TorServiceInitializationError as e:
error_msg = f"[USE TOR SERVICE] Tor failed to start: {e}"
tor_module.destroy_session(port_number)
logger.error(error_msg, exc_info=True)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except Exception as e:
logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}")
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
proxies = { proxies = {
"http": f"socks5h://127.0.0.1:{port_number}", "http": f"socks5h://127.0.0.1:{port_number}",
"https": f"socks5h://127.0.0.1:{port_number}", "https": f"socks5h://127.0.0.1:{port_number}",
} }
logger.debug(f"Sending data through Tor via port {port_number}") logger.debug(f"Sending data through Tor via port {port_number}")
# ============================================================================
# USE TOR AS PROXY
# ============================================================================
try:
response = requests.post(custom_url, json=payload, proxies=proxies) response = requests.post(custom_url, json=payload, proxies=proxies)
logger.debug("Sending data through Tor successful.") logger.debug("[USE TOR SERVICE] Sent data through Tor successfully. Now deleting the session..")
tor_module.destroy_session(port_number) tor_module.destroy_session(port_number)
return response return response
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
error_msg = "There was a Connection Error with the Tor Module." error_msg = "[USE TOR SERVICE] There was a Connection Error with the Use Tor Module."
logger.error(error_msg, exc_info=True) logger.error(error_msg, exc_info=True)
logger.debug(error_msg) logger.debug(error_msg)
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem( problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer custom_url, connection_observer
) )
return problem_results return problem_results
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
error_msg = "Connection timed out" error_msg = "[USE TOR SERVICE] Connection timed out"
logger.error(error_msg, exc_info=True) logger.error(error_msg, exc_info=True)
logger.debug(error_msg) logger.debug(error_msg)
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem( problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer custom_url, connection_observer
) )
@ -122,10 +188,28 @@ def tor_post_request(
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
if isinstance(response, requests.Response): if isinstance(response, requests.Response):
error_msg = f"HTTP error: {response.status_code}" error_msg = f"[USE TOR SERVICE] HTTP error: {response.status_code}"
logger.error(error_msg, exc_info=True) logger.error(error_msg, exc_info=True)
logger.debug(error_msg) logger.debug(error_msg)
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except requests.exceptions.ProxyError:
error_msg = "[USE TOR SERVICE] Requests Proxy Error"
logger.error(error_msg, exc_info=True)
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer
)
return problem_results
except Exception as e:
logger.error(f"[USE TOR SERVICE] Unexpected Tor error: {e}")
tor_module.destroy_session(port_number)
problem_results = evaluate_tor_networking_problem( problem_results = evaluate_tor_networking_problem(
custom_url, connection_observer custom_url, connection_observer
) )

View file

@ -5,6 +5,7 @@ from core.models.orm_models.Base import Base
from core.observers.ClientObserver import ClientObserver from core.observers.ClientObserver import ClientObserver
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.errors.logger import logger from core.errors.logger import logger
from core.utils.basic_operations.get_parent_directory import get_parent_directory
from typing import Optional from typing import Optional
import json import json
@ -29,8 +30,11 @@ def sync_one_orm_model(
new_data = api_result.get("data") new_data = api_result.get("data")
# prep data (denormalize) # prep data (denormalize)
parent_directory = get_parent_directory()
yaml_filename = f"{which_endpoint}.yaml" yaml_filename = f"{which_endpoint}.yaml"
denormalized_data = denormalize(new_data, yaml_filename) full_yaml_path = f"{parent_directory}/assets/yaml_mappings/{yaml_filename}"
denormalized_data = denormalize(new_data, str(full_yaml_path))
if denormalized_data: if denormalized_data:
# Debug Pretty print with indentation # Debug Pretty print with indentation

View file

@ -0,0 +1,8 @@
from pathlib import Path
import core
def get_parent_directory():
# Get core's parent directory
parent_directory = Path(core.__file__).parent.parent
return str(parent_directory)

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-core" name = "sp-hydra-veil-core"
version = "2.3.6" version = "2.3.8"
authors = [ authors = [
{ name = "Simplified Privacy" }, { name = "Simplified Privacy" },
] ]
@ -19,7 +19,7 @@ dependencies = [
"pysocks ~= 1.7.1", "pysocks ~= 1.7.1",
"python-dateutil ~= 2.9.0.post0", "python-dateutil ~= 2.9.0.post0",
"requests ~= 2.32.5", "requests ~= 2.32.5",
"sp-essentials ~= 1.1.0", "sp-essentials ~= 1.2.0",
"annotated-types==0.7.0", "annotated-types==0.7.0",
"certifi==2026.4.22", "certifi==2026.4.22",
"charset-normalizer==3.4.7", "charset-normalizer==3.4.7",
@ -38,6 +38,7 @@ dependencies = [
"pydantic_core==2.46.3", "pydantic_core==2.46.3",
"pydeps==3.0.6", "pydeps==3.0.6",
"pytokens==0.4.1", "pytokens==0.4.1",
"PyYAML==6.0.3",
"stdlib-list==0.12.0", "stdlib-list==0.12.0",
"SQLAlchemy==2.0.51", "SQLAlchemy==2.0.51",
"toolz==1.1.0", "toolz==1.1.0",