diff --git a/.gitignore b/.gitignore index 31df78a..8b3c82c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.dev prototype_client.py .idea .venv diff --git a/assets/yaml_mappings/locations.yaml b/assets/yaml_mappings/locations.yaml new file mode 100644 index 0000000..762af31 --- /dev/null +++ b/assets/yaml_mappings/locations.yaml @@ -0,0 +1,49 @@ +fields: + - name: country_code + path: ['country', 'code'] + required: true + + - name: code # this is city code + path: ['code'] + required: true + + - name: id + path: ['id'] + + - name: country_name + path: ['country', 'name'] + + - name: name # this is CITY name + path: ['name'] + + - name: time_zone + path: ['time_zone', 'code'] + + - name: operator_id + path: ['operator_id'] + required: true + + - name: provider_name + path: ['provider', 'name'] + + - name: is_proxy_capable + path: ['is_proxy_capable'] + + - name: is_wireguard_capable + path: ['is_wireguard_capable'] + required: true + + + # required: true + + # locations.append(( + # location['country']['code'], + # location['code'], + # location['id'], + # location['country']['name'], + # location['name'], + # location['time_zone']['code'], + # location['operator_id'], + # location['provider']['name'], + # location['is_proxy_capable'], + # location['is_wireguard_capable'])) diff --git a/assets/yaml_mappings/mapping_one.yaml b/assets/yaml_mappings/mapping_one.yaml new file mode 100644 index 0000000..903eaba --- /dev/null +++ b/assets/yaml_mappings/mapping_one.yaml @@ -0,0 +1,8 @@ +fields: + - name: group_a_code + path: ['group_a', 'code'] + - name: code + path: ['code'] + - name: id + path: ['id'] + required: true diff --git a/assets/yaml_mappings/operators.yaml b/assets/yaml_mappings/operators.yaml new file mode 100644 index 0000000..f7e9b8f --- /dev/null +++ b/assets/yaml_mappings/operators.yaml @@ -0,0 +1,19 @@ +fields: + - name: id + path: ['id'] + required: true + + - name: name + path: ['name'] + + - name: public_key # this is ed25519 + path: ['public_key'] + + - name: nostr_public_key + path: ['nostr_public_key'] + + - name: nostr_profile_reference + path: ['nostr_profile_reference'] + + - name: nostr_attestation_event_reference + path: ['nostr_attestation', 'event_reference'] diff --git a/core/.metadata.md b/core/.metadata.md new file mode 100644 index 0000000..86fc4bb --- /dev/null +++ b/core/.metadata.md @@ -0,0 +1 @@ +this is june 13 version diff --git a/core/controllers/ClientController.py b/core/controllers/ClientController.py index d47eb9e..85b9a2e 100644 --- a/core/controllers/ClientController.py +++ b/core/controllers/ClientController.py @@ -1,3 +1,7 @@ +# new sync refactor: +from core.services.sync.sync_service import coordinate_cache_sync +from core.errors.logger import logger +# prior versions: from core.Constants import Constants from core.Errors import UnknownClientPathError, UnknownClientVersionError, CommandNotFoundError from core.controllers.ApplicationController import ApplicationController @@ -44,8 +48,40 @@ class ClientController: @staticmethod def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None): - from core.controllers.ConnectionController import ConnectionController - ConnectionController.with_preferred_connection(task=ClientController.__sync, client_observer=client_observer, connection_observer=connection_observer) + from core.models.manage.session_management import init_session, close_session + + # Use Cached Method: + init_session() + result = coordinate_cache_sync(ClientObserver, ConnectionObserver) + + logger.info(f"We got a Result from the API of {result}") + + close_session() + + # Outright Error: + if not result["success"]: + error_msg = result["error"] + if client_observer is not None: + client_observer.notify('synchronizing', f'Error! {error_msg}') + return + + # Same: + changed_tables = result["changed_tables"] + if not changed_tables: + if client_observer is not None: + client_observer.notify('synchronized') + return + + # We only make it past this point if there's New Data + + # Fetch and update those models... + try: + from core.controllers.ConnectionController import ConnectionController + ConnectionController.with_preferred_connection(task=ClientController.__sync, changed_tables=changed_tables, client_observer=client_observer, connection_observer=connection_observer) + except: + # sync failed here, + if client_observer is not None: + client_observer.notify('synchronizing', 'Error! Sync Failed.') @staticmethod def update(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None): @@ -61,32 +97,62 @@ class ClientController: return path + @staticmethod - def __sync(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None): + def __sync(changed_tables: list, client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None): if client_observer is not None: client_observer.notify('synchronizing') - # noinspection PyProtectedMember - ApplicationController._sync(proxies=proxies) - # noinspection PyProtectedMember - ApplicationVersionController._sync(proxies=proxies) - # noinspection PyProtectedMember - ClientVersionController._sync(proxies=proxies) - # noinspection PyProtectedMember - OperatorController._sync(proxies=proxies) - # noinspection PyProtectedMember - LocationController._sync(proxies=proxies) - # noinspection PyProtectedMember - SubscriptionPlanController._sync(proxies=proxies) + if "applications" in changed_tables: + logger.info("Sync applications..") + if client_observer is not None: + client_observer.notify('synchronizing', 'Fetching Browser List') + # noinspection PyProtectedMember + ApplicationController._sync(proxies=proxies) + + if "application_versions" in changed_tables: + logger.info("Sync Application Versions..") + if client_observer is not None: + client_observer.notify('synchronizing', 'Fetching Browser Version List') + # noinspection PyProtectedMember + ApplicationVersionController._sync(proxies=proxies) + + if "client_version" in changed_tables: + logger.info("Sync of client version") + if client_observer is not None: + client_observer.notify('synchronizing', 'Fetching Client Version List') + # noinspection PyProtectedMember + ClientVersionController._sync(proxies=proxies) + + if "operators" in changed_tables: + logger.info("Sync of Operators") + if client_observer is not None: + client_observer.notify('synchronizing', 'Fetching Operators List') + # noinspection PyProtectedMember + OperatorController._sync(proxies=proxies) + + if "locations" in changed_tables: + logger.info("Sync of Locations") + if client_observer is not None: + client_observer.notify('synchronizing', 'Fetching Locations List') + # noinspection PyProtectedMember + LocationController._sync(proxies=proxies) + + if "subscriptions" in changed_tables: + logger.info("Sync of Subscriptions") + if client_observer is not None: + client_observer.notify('synchronizing', 'Fetching Subscription List') + # noinspection PyProtectedMember + SubscriptionPlanController._sync(proxies=proxies) ConfigurationController.update_last_synced_at() + logger.info("Entire Sync Completed Successfully") if client_observer is not None: client_observer.notify('synchronized') @staticmethod - def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None): if ClientController.can_be_updated(): diff --git a/core/controllers/ConnectionController.py b/core/controllers/ConnectionController.py index bc380e4..f4a7686 100644 --- a/core/controllers/ConnectionController.py +++ b/core/controllers/ConnectionController.py @@ -29,6 +29,13 @@ class ConnectionController: @staticmethod def with_preferred_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs): + """ + This function does a task with a preferred connection type. + + It is currently being CALLED UPON with keyword based kwargs only, because any args would execute immediately. + + However, the *args keeps it open to future uses. + """ connection = ConfigurationController.get_connection() diff --git a/core/models/manage/get_from_model.py b/core/models/manage/get_from_model.py new file mode 100644 index 0000000..f077639 --- /dev/null +++ b/core/models/manage/get_from_model.py @@ -0,0 +1,35 @@ +from core.models.manage.session_management import get_session +from typing import Type, List, Dict, Any +from sqlalchemy.exc import SQLAlchemyError + +def get_from_model(model_class: Type, **filters) -> List[Dict[str, Any]]: + """ + Retrieve data from a model. Keeping this generic for reuse + + Args: + model_class: The ORM model class + **filters: Optional column=value pairs to filter by (e.g., id=1, name='John') + + Returns: + List of dictionaries representing matching rows + """ + with get_session() as session: + try: + query = session.query(model_class) + + # Apply filters dynamically + if filters: + for column, value in filters.items(): + if hasattr(model_class, column): + query = query.filter(getattr(model_class, column) == value) + + # Convert to list of dicts + results = [ + {c.name: getattr(row, c.name) for c in row.__table__.columns} + for row in query.all() + ] + return results + + except SQLAlchemyError as e: + raise ValueError(f"Query failed: {e}") + diff --git a/core/models/manage/insert.py b/core/models/manage/insert.py new file mode 100644 index 0000000..8b3520e --- /dev/null +++ b/core/models/manage/insert.py @@ -0,0 +1,61 @@ +from core.errors.logger import logger +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine +from typing import Type, Dict, Any +from sqlalchemy.exc import IntegrityError, SQLAlchemyError +from core.models.orm_models.Base import Base +from core.models.orm_models.CachedSync import CachedSync +from core.models.manage.session_management import get_session + +def insert_into_model(model_class: Type, all_data: dict | list, override=False) -> bool: + """ + Generic ORM insert for any model. Keeping it generic for reuse. + + Args: + model_class: The ORM model class + all_data: Dictionary or list of dictionaries with data matching the model + override: If True, wipe the table before inserting + + Returns: + True if successful + """ + + logger.info(f"All the public data preparing to be inserted is {all_data}") + + with get_session() as session: + # Step 1: Wipe the table if override=True + if override: + logger.info(f"First, WIPING the pre-existing data for {model_class.__name__}. Are you sure you intended to completely delete the old data?") + session.query(model_class).delete() + session.commit() + + logger.info(f"Starting insert for {model_class.__name__}") + + # Normalize to list for uniform handling + data_list = all_data if isinstance(all_data, list) else [all_data] + + try: + for each_json in data_list: + instance = model_class(**each_json) + session.add(instance) + + session.commit() + logger.info(f"Completed! SQL Insertion Successful following the {model_class.__name__} Model’s Type Rules") + return True + + except TypeError as e: + print(f"TypeError caught: {e}") + session.rollback() + raise ValueError(f"Invalid fields for {model_class.__name__}: {e}") + except IntegrityError as e: + print(f"IntegrityError caught: {e.orig}") + session.rollback() + raise ValueError(f"Constraint violation: {e.orig}") + except SQLAlchemyError as e: + print(f"SQLAlchemyError caught: {e}") + session.rollback() + raise + except Exception as e: + print(f"Generic exception caught: {e}") + session.rollback() + raise e diff --git a/core/models/manage/session_management.py b/core/models/manage/session_management.py new file mode 100644 index 0000000..9f9d226 --- /dev/null +++ b/core/models/manage/session_management.py @@ -0,0 +1,40 @@ +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine +from core.models.orm_models.Base import Base +from core.Constants import Constants + +database_path = Constants.HV_STORAGE_DATABASE_PATH +engine = create_engine(f"sqlite:///{database_path}") +Session = sessionmaker(bind=engine) + +_session = None + +def init_session(): + """ + Called once at application startup using global variables for the session, + which initialize prior to being called with a false flag. + + The reason for this is so the app can load prior to be called, + while keeping it a globally accessible variable + """ + global _session + Base.metadata.create_all(engine) + _session = Session() + +def get_session(): + """Get the persistent session""" + if _session is None: + raise RuntimeError("Session not initialized. Call init_session() first.") + return _session + +def close_session(): + """ + Used prior to shutting down the application to break SQL connections. + + In the current refactor, it may be used prior to shut down, such as sync completion, + because a low amount of the total models currently use the new system. + """ + global _session + if _session: + _session.close() + _session = None diff --git a/core/models/orm_models/Base.py b/core/models/orm_models/Base.py new file mode 100644 index 0000000..f2c489f --- /dev/null +++ b/core/models/orm_models/Base.py @@ -0,0 +1,9 @@ +# base.py +from sqlalchemy.orm import declarative_base + +Base = declarative_base() + +""" +This will eventually replace the legacy base model, +but is still required, even if empty, to setup the declarative base. +""" diff --git a/core/models/orm_models/CachedSync.py b/core/models/orm_models/CachedSync.py new file mode 100644 index 0000000..1d626cf --- /dev/null +++ b/core/models/orm_models/CachedSync.py @@ -0,0 +1,24 @@ +from sqlalchemy import Integer, String, ForeignKey +from sqlalchemy.orm import declarative_base, mapped_column, Mapped, relationship +from typing import Optional +from sqlalchemy.orm import Mapped +from core.models.orm_models.Base import Base + +""" +This is one of the SQLAlchemy Models from the refactor. + +That’s why it lacks functions to get or insert data. +""" + +class CachedSync(Base): + __tablename__ = 'cached_sync' + + # version of the cached sync itself is the primary key + version: Mapped[int] = mapped_column(Integer, primary_key=True) + + applications: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None) + application_versions: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None) + client_version: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None) + operators: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None) + locations: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None) + subscriptions: Mapped[Optional[str]] = mapped_column(Integer, nullable=True, default=None) diff --git a/core/services/WebServiceApiService.py b/core/services/WebServiceApiService.py index b216915..55ea9ad 100644 --- a/core/services/WebServiceApiService.py +++ b/core/services/WebServiceApiService.py @@ -212,3 +212,15 @@ class WebServiceApiService: headers = None return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies) + + @staticmethod + def get_cached_sync(proxies: Optional[dict] = None): + + from requests.status_codes import codes as status_codes + + response = WebServiceApiService.__get('/cachedsync', None, proxies) + + if response.status_code == status_codes.OK: + return response.json() + else: + return None \ No newline at end of file diff --git a/core/services/networking/make_url.py b/core/services/networking/make_url.py index 802d9d8..3b9c67b 100644 --- a/core/services/networking/make_url.py +++ b/core/services/networking/make_url.py @@ -5,8 +5,5 @@ from core.Constants import Constants def make_url(which_endpoint: str) -> str: domain = Constants.TICKET_API_BASE_URL - # hardcode option: - # domain = "https://ticket.hydraveil.net" - final_url = f"{domain}/{which_endpoint}" return final_url diff --git a/core/services/networking/regular_get_request.py b/core/services/networking/regular_get_request.py index 1361a04..a9d8e17 100644 --- a/core/services/networking/regular_get_request.py +++ b/core/services/networking/regular_get_request.py @@ -15,6 +15,7 @@ def regular_get_request(url: str, connection_observer: ConnectionObserver): import requests try: + print(f"we are doing a regular GET request to: {url}") response = requests.get(url) # Raises HTTPError for 4xx/5xx diff --git a/core/services/sync/compare_tables.py b/core/services/sync/compare_tables.py new file mode 100644 index 0000000..f25d10e --- /dev/null +++ b/core/services/sync/compare_tables.py @@ -0,0 +1,70 @@ + +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + + +def compare_tables(new_data: dict, old_table: dict) -> tuple[list[str], list[str]]: + """ + Compare version numbers in new and old metadata dictionaries. + + Args: + new_data: Latest metadata from API (e.g., {"version": 2, "model_one": 2}) + old_table: Previously cached metadata from database + + Returns: + Tuple of (changed_tables, new_model_types) + - changed_tables: model keys where version increased + - new_model_types: model keys that don't exist in old_table (reserved for future use) + + Raises: + TypeError: If either argument is not a dictionary + ValueError: If a model version is not an integer + """ + # Validate inputs + if not isinstance(new_data, dict) or not isinstance(old_table, dict): + raise TypeError("Both new_data and old_table must be dictionaries") + + changed_tables = [] + new_model_types = [] + + for model_key, new_version in new_data.items(): + # Skip the "version" metadata key itself + if model_key == "version": + continue + + # Type check: versions must be integers + if not isinstance(new_version, int): + raise ValueError( + f"Model '{model_key}' has non-integer version: {new_version!r}. " + f"Expected int." + ) + + # Identify new model types (not in old data) + if model_key not in old_table: + new_model_types.append(model_key) + continue + + # Identify changed models (version increased) + if new_version > old_table[model_key]: + changed_tables.append(model_key) + + return changed_tables, new_model_types + +# ============== ISOLATION TESTING ================== +# SPOOF ISOLATION TEST: +# new_data = { +# "a": 1, +# "b": 2, +# "c": 4, +# "d": 1 +# } +# old_data = { +# "a": 1, +# "b": 2, +# "c": 3, +# } +# changed_tables, new_data_types = compare_tables(new_data, old_data) +# print(f"changed tables: {changed_tables}") +# print(f"new data types: {new_data_types}") diff --git a/core/services/sync/sync_service.py b/core/services/sync/sync_service.py new file mode 100644 index 0000000..43bf810 --- /dev/null +++ b/core/services/sync/sync_service.py @@ -0,0 +1,311 @@ +# from __future__ import annotations +# from typing import TYPE_CHECKING + +# if TYPE_CHECKING: +# from core.essentials.observers.ConnectionObserver import ConnectionObserver + # from core.observers.TicketObserver import TicketObserver + +# comparisons: +from core.services.sync.compare_tables import compare_tables + +# unique database: +from core.models.manage.session_management import get_session +from core.models.manage.get_from_model import get_from_model +from core.models.manage.insert import insert_into_model +from core.models.orm_models.CachedSync import CachedSync + +from core.Constants import Constants +from core.observers.BaseObserver import BaseObserver +from core.services.networking.get_data_from_server import get_data_from_server +from core.errors.logger import logger +from core.observers.ClientObserver import ClientObserver +from core.observers.ConnectionObserver import ConnectionObserver + +# generic +from sqlalchemy import func +from typing import Optional +from typing import Optional + + +def _get_cached_metadata(): + """Retrieve cached metadata from the database. + + Returns: + tuple: (old_data dict, previous_highest version number) + If no cache exists: ({}, 0) + + Raises: + Exception: Any database query errors propagate up. + """ + old_data_as_list = get_from_model(CachedSync) + + if not old_data_as_list: + logger.warning("No cached sync data in database. Treating as first sync.") + return {}, 0 + + if not isinstance(old_data_as_list, list): + logger.critical("Error with get_from_model improperly returning non-lists.") + return {}, 0 + + old_data = old_data_as_list[0] + previous_highest = old_data.get("version", 0) + logger.debug(f"Cached metadata version: {previous_highest}") + + return old_data, previous_highest + + +def _get_changed_models(new_data, old_data): + """Determine which models changed between API and cached versions. + + Args: + new_data: Dict of new metadata from API + old_data: Dict of old metadata from cache (empty dict if first sync) + + Returns: + tuple: (changed_tables list, new_model_types list) + + Raises: + TypeError: If inputs have invalid structure + ValueError: If version values are invalid + """ + if not old_data: + # First sync: no baseline, treat all models as needing fetch + changed_tables = [key for key in new_data if key != "version"] + new_model_types = [] + logger.info(f"First sync: fetching all models: {changed_tables}") + else: + # Subsequent sync: compare against baseline + changed_tables, new_model_types = compare_tables(new_data, old_data) + + return changed_tables, new_model_types + + +def _filter_valid_models(data, invalid_keys): + """Remove invalid model types from data dict. + + Args: + data: Dict of metadata + invalid_keys: List of keys to exclude + + Returns: + Dict with invalid keys removed + """ + filtered = {} + for key, value in data.items(): + if key not in invalid_keys: + filtered[key] = value + return filtered + + +def save_data(data: dict) -> bool: + """Save metadata to database.""" + try: + logger.debug(f"Inserting metadata into database: {data}") + did_it_work = insert_into_model(CachedSync, data, True) + + if did_it_work: + logger.debug("Metadata saved successfully") + else: + logger.warning("insert_into_model returned False") + + return did_it_work + + except Exception as e: + logger.error(f"Failed to save metadata to database: {str(e)}", exc_info=True) + return False + + +def _log_what_changed(new_model_types, changed_tables): + if new_model_types: + logger.debug(f"New model types detected (reserved): {new_model_types}") + + if changed_tables: + logger.info(f"Models to sync: {changed_tables}") + else: + logger.info("No models changed between versions.") + + +# Highest Level in this Flow: +def coordinate_cache_sync( + client_observer: Optional[ClientObserver] = None, + connection_observer: Optional[ConnectionObserver] = None +) -> dict: + """ + Fetch version metadata from API and return changed/new model keys. + + Returns: + Dictionary with keys: + - 'success' (bool): Whether comparison succeeded + - 'changed_tables' (list): Model keys that changed + - 'new_model_types' (list): New model keys (reserved for future use) + - 'error' (str or None): Error message if success is False + """ + + api_result = _get_sync_cache_from_api(ClientObserver, ConnectionObserver) + + # we trust the '_get_sync_cache_from_api' function to give us a dictionary: + if not api_result.get("success"): + error_msg = api_result.get("error", "Unknown API error") + logger.error(f"API sync failed: {error_msg}") + return { + "success": False, + "changed_tables": [], + "new_model_types": [], + "error": error_msg, + } + + # Extract the data from the client's own trusted function, + new_data = api_result.get("data") + + # Validate the API's payload, (we don't trust the API's structure) + if not isinstance(new_data, dict): + error_msg = "API returned invalid metadata: 'data' is not a dict" + logger.error(error_msg) + return { + "success": False, + "changed_tables": [], + "new_model_types": [], + "error": error_msg, + } + + if "version" not in new_data: + error_msg = "API returned invalid metadata: missing 'version' key" + logger.error(error_msg) + return { + "success": False, + "changed_tables": [], + "new_model_types": [], + "error": error_msg, + } + + + new_highest = new_data.get("version") + logger.debug(f"API metadata version: {new_highest}") + + # Fetch old cached version from database + try: + old_data, previous_highest = _get_cached_metadata() + + except Exception as e: + error_msg = f"Database query failed: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + "success": False, + "changed_tables": [], + "new_model_types": [], + "error": error_msg, + } + + # Version hasn't changed + if new_highest == previous_highest: + logger.info(f"Versions match (both {new_highest}). No sync needed.") + return { + "success": True, + "changed_tables": [], + "new_model_types": [], + "error": None, + } + + # Determine which models changed + try: + changed_tables, new_model_types = _get_changed_models(new_data, old_data) + + except (TypeError, ValueError) as e: + error_msg = f"Invalid metadata: {str(e)}" + logger.error(error_msg) + return { + "success": False, + "changed_tables": [], + "new_model_types": [], + "error": error_msg, + } + + # this is not saving it to SQL, it's a temp debug log: + _log_what_changed(new_model_types, changed_tables) + + # Filter out new model types for the upcoming save: + filtered_data = _filter_valid_models(new_data, new_model_types) + + # Save only currently existing models to SQL: + save_successful = save_data(filtered_data) + if save_successful: + return { + "success": True, + "changed_tables": changed_tables, + "new_model_types": new_model_types, + "error": None, + } + else: + return { + "success": False, + "changed_tables": [], + "new_model_types": [], + "error": "Failed to save metadata to database", + } + + + + +def _get_sync_cache_from_api( + client_observer: Optional[ClientObserver] = None, + connection_observer: Optional[ConnectionObserver] = None +) -> dict: + logger.info("Syncing with the API...") + + # Get and validate the base URL + rejected_list = [None, False, ""] + base_url = Constants.SP_API_BASE_URL + if base_url in rejected_list: + error_msg = "Invalid base URL from configuration" + logger.error(error_msg) + return {"success": False, "data": None, "error": error_msg} + + # Construct the full endpoint URL + url = f"{base_url}/cachedsync" + logger.debug(f"API endpoint: {url}") + + try: + logger.debug("Fetching data from API server...") + sync_results = get_data_from_server(url, connection_observer) + + if sync_results in rejected_list: + error_msg = "API returned no data" + logger.error(error_msg) + return {"success": False, "data": None, "error": error_msg} + + logger.debug(f"Raw API response: {sync_results}") + + final_result = sync_results.get("data", None) + + if final_result: + logger.info("Successfully retrieved sync versions metadata from API") + return {"success": True, "data": final_result, "error": None} + + error_msg = "API response missing 'data' field" + logger.error(error_msg) + return {"success": False, "data": None, "error": error_msg} + + except Exception as e: + error_msg = f"API fetch failed: {str(e)}" + logger.error(error_msg) + return {"success": False, "data": None, "error": error_msg} + + + + + + +# ============== ISOLATION TESTING ================== + +# SPOOF ISOLATION TEST: +# def _get_sync_cache_from_api( +# final_result = { +# "version": 5, +# "applications": 3, +# "application_versions": 2, +# "client_version": 2, +# "operators": 2, +# "locations": 2, +# "subscriptions": 1 +# } +# return {"success": True, "data": final_result, "error": None} diff --git a/prototype_sync.py b/prototype_sync.py new file mode 100644 index 0000000..7d14b40 --- /dev/null +++ b/prototype_sync.py @@ -0,0 +1,40 @@ +from core.controllers.ClientController import ClientController +from core.observers.ClientObserver import ClientObserver +from core.observers.ConnectionObserver import ConnectionObserver +from core.models.manage.session_management import init_session, close_session + +class PrototypeSync(): + def __init__(self, parent=None): + client_observer = ClientObserver() + connection_observer = ConnectionObserver() + client_observer.subscribe('synchronizing', lambda event: print('Synchronizing...\n')) + client_observer.subscribe('updating', lambda event: print('Updating client...')) + client_observer.subscribe('update_progressing', lambda event: print(f'Current progress: {event.meta.get('progress'):.2f}%', flush=True, end='\r')) + client_observer.subscribe('updated', lambda event: print('\n')) + + connection_observer.subscribe('connecting', lambda event: self.update_status( + f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...')) + + connection_observer.subscribe('tor_bootstrapping', lambda event: self.update_status( + 'Establishing Tor connection...')) + + connection_observer.subscribe('tor_bootstrap_progressing', lambda event: self.update_status( + f'Bootstrapping Tor {event.meta.get('progress'):.2f}%')) + + connection_observer.subscribe( + 'tor_bootstrapped', lambda event: self.update_status('Tor connection established.')) + + + init_session() + + print("Sync..") + ClientController.sync(client_observer=client_observer, connection_observer=connection_observer) + print("Done") + + # On app shutdown + close_session() + + + def update_status(self, text, clear=False): + if text: + print(text) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f89aa21..c12431b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sp-hydra-veil-core" -version = "2.3.4" +version = "2.3.5" authors = [ { name = "Simplified Privacy" }, ]