Sync structure changes
This commit is contained in:
parent
8e2ea64b6b
commit
330bf13465
20 changed files with 767 additions and 20 deletions
49
assets/yaml_mappings/locations.yaml
Normal file
49
assets/yaml_mappings/locations.yaml
Normal file
|
|
@ -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']))
|
||||||
8
assets/yaml_mappings/mapping_one.yaml
Normal file
8
assets/yaml_mappings/mapping_one.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fields:
|
||||||
|
- name: group_a_code
|
||||||
|
path: ['group_a', 'code']
|
||||||
|
- name: code
|
||||||
|
path: ['code']
|
||||||
|
- name: id
|
||||||
|
path: ['id']
|
||||||
|
required: true
|
||||||
19
assets/yaml_mappings/operators.yaml
Normal file
19
assets/yaml_mappings/operators.yaml
Normal file
|
|
@ -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']
|
||||||
|
|
@ -10,7 +10,8 @@ class Constants:
|
||||||
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
|
"TICKET_API_BASE_URL", "https://ticket.hydraveil.net"
|
||||||
)
|
)
|
||||||
|
|
||||||
SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://api.hydraveil.net/api/v1')
|
SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://dev-us-002.simplifiedprivacy.net/api/v1')
|
||||||
|
# SP_API_BASE_URL: Final[str] = os.environ.get('SP_API_BASE_URL', 'https://api.hydraveil.net/api/v1')
|
||||||
PING_URL: Final[str] = os.environ.get('PING_URL', 'https://api.hydraveil.net/api/v1/health')
|
PING_URL: Final[str] = os.environ.get('PING_URL', 'https://api.hydraveil.net/api/v1/health')
|
||||||
|
|
||||||
CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5'))
|
CONNECTION_RETRY_INTERVAL: Final[int] = int(os.environ.get('CONNECTION_RETRY_INTERVAL', '5'))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from core.services.sync.sync_service import coordinate_cache_sync
|
||||||
|
|
||||||
from core.Constants import Constants
|
from core.Constants import Constants
|
||||||
from core.Errors import UnknownClientPathError, UnknownClientVersionError, CommandNotFoundError
|
from core.Errors import UnknownClientPathError, UnknownClientVersionError, CommandNotFoundError
|
||||||
from core.controllers.ApplicationController import ApplicationController
|
from core.controllers.ApplicationController import ApplicationController
|
||||||
|
|
@ -44,8 +46,38 @@ class ClientController:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
|
def sync(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
|
||||||
|
|
||||||
from core.controllers.ConnectionController import ConnectionController
|
from core.models.manage.session_management import init_session, close_session
|
||||||
ConnectionController.with_preferred_connection(task=ClientController.__sync, client_observer=client_observer, connection_observer=connection_observer)
|
init_session()
|
||||||
|
print("I sync'ed")
|
||||||
|
|
||||||
|
# Use Cached Method:
|
||||||
|
changed_tables = coordinate_cache_sync(ClientObserver, ConnectionObserver)
|
||||||
|
|
||||||
|
# SPOOFING
|
||||||
|
# changed_tables = ['version', 'applications', 'application_versions', 'client_version', 'operators', 'locations', 'subscriptions']
|
||||||
|
|
||||||
|
print(f"changed_tables: {changed_tables}")
|
||||||
|
|
||||||
|
# this should be same or any errors:
|
||||||
|
if isinstance(changed_tables, dict):
|
||||||
|
# check result is "same"?
|
||||||
|
# or an errro
|
||||||
|
if client_observer is not None:
|
||||||
|
client_observer.notify('synchronized')
|
||||||
|
|
||||||
|
# now it's likely to have results:
|
||||||
|
if isinstance(changed_tables, list):
|
||||||
|
quantity_of_changes = len(changed_tables)
|
||||||
|
|
||||||
|
print(f"quantity_of_changes is {quantity_of_changes}")
|
||||||
|
|
||||||
|
# sanity check:
|
||||||
|
if quantity_of_changes >= 0:
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
if client_observer is not None:
|
||||||
|
client_observer.notify('synchronized')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
|
def update(client_observer: ClientObserver = None, connection_observer: ConnectionObserver = None):
|
||||||
|
|
@ -61,24 +93,46 @@ class ClientController:
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
# def cached_sync(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
|
||||||
|
|
||||||
@staticmethod
|
@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):
|
||||||
|
|
||||||
|
# SPOOFING
|
||||||
|
# changed_tables = ['version', 'applications', 'application_versions', 'client_version', 'operators', 'locations', 'subscriptions']
|
||||||
|
|
||||||
if client_observer is not None:
|
if client_observer is not None:
|
||||||
client_observer.notify('synchronizing')
|
client_observer.notify('synchronizing')
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
if "applications" in changed_tables:
|
||||||
ApplicationController._sync(proxies=proxies)
|
print("sync applications")
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
ApplicationVersionController._sync(proxies=proxies)
|
ApplicationController._sync(proxies=proxies)
|
||||||
# noinspection PyProtectedMember
|
|
||||||
ClientVersionController._sync(proxies=proxies)
|
if "application_versions" in changed_tables:
|
||||||
# noinspection PyProtectedMember
|
print("sync application_versions")
|
||||||
OperatorController._sync(proxies=proxies)
|
# noinspection PyProtectedMember
|
||||||
# noinspection PyProtectedMember
|
ApplicationVersionController._sync(proxies=proxies)
|
||||||
LocationController._sync(proxies=proxies)
|
|
||||||
# noinspection PyProtectedMember
|
if "client_version" in changed_tables:
|
||||||
SubscriptionPlanController._sync(proxies=proxies)
|
print("sync client_version")
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
ClientVersionController._sync(proxies=proxies)
|
||||||
|
|
||||||
|
if "operators" in changed_tables:
|
||||||
|
print("sync operators")
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
OperatorController._sync(proxies=proxies)
|
||||||
|
|
||||||
|
if "locations" in changed_tables:
|
||||||
|
print("sync locations")
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
LocationController._sync(proxies=proxies)
|
||||||
|
|
||||||
|
if "subscriptions" in changed_tables:
|
||||||
|
print("sync subscriptions")
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
SubscriptionPlanController._sync(proxies=proxies)
|
||||||
|
|
||||||
ConfigurationController.update_last_synced_at()
|
ConfigurationController.update_last_synced_at()
|
||||||
|
|
||||||
|
|
@ -86,7 +140,6 @@ class ClientController:
|
||||||
client_observer.notify('synchronized')
|
client_observer.notify('synchronized')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
||||||
def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
|
def __update(client_observer: Optional[ClientObserver] = None, proxies: Optional[dict] = None):
|
||||||
|
|
||||||
if ClientController.can_be_updated():
|
if ClientController.can_be_updated():
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ class ConnectionController:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def with_preferred_connection(*args, task: Callable[..., Any], connection_observer: Optional[ConnectionObserver] = None, **kwargs):
|
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()
|
connection = ConfigurationController.get_connection()
|
||||||
|
|
||||||
|
|
|
||||||
35
core/models/manage/get_from_model.py
Normal file
35
core/models/manage/get_from_model.py
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
138
core/models/manage/insert.py
Normal file
138
core/models/manage/insert.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
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.python_models.Base import Base
|
||||||
|
from core.models.python_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.
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(f"all_data is {all_data}")
|
||||||
|
|
||||||
|
with get_session() as session:
|
||||||
|
# Step 1: Wipe the table if override=True
|
||||||
|
if override:
|
||||||
|
session.query(model_class).delete()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
print(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()
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# def insert_into_model(model_class: Type, all_data: list, override=False) -> Any:
|
||||||
|
# """
|
||||||
|
# Generic ORM insert for any model. But not used for the legacy codebase yet.
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# model_class: The ORM model class
|
||||||
|
# json_data: Dictionary with data matching the model
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# The created instance
|
||||||
|
# """
|
||||||
|
|
||||||
|
# print(f"all_data is {all_data}")
|
||||||
|
|
||||||
|
# with get_session() as session:
|
||||||
|
# # Step 1: Wipe the table if override=True
|
||||||
|
# if override:
|
||||||
|
# session.query(model_class).delete()
|
||||||
|
# session.commit()
|
||||||
|
|
||||||
|
# print("we made it past getting the session")
|
||||||
|
|
||||||
|
# print(f"Starting insert for {model_class.__name__}")
|
||||||
|
|
||||||
|
# type_of_data = type(all_data)
|
||||||
|
# print(f"all_data type is: {type_of_data}")
|
||||||
|
|
||||||
|
# if isinstance(all_data, dict):
|
||||||
|
# try:
|
||||||
|
# instance = model_class(**all_data)
|
||||||
|
# session.add(instance)
|
||||||
|
# session.commit()
|
||||||
|
# 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
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# elif isinstance(all_data, list):
|
||||||
|
# for each_json in all_data:
|
||||||
|
# try:
|
||||||
|
# instance = model_class(**each_json)
|
||||||
|
# session.add(instance)
|
||||||
|
# session.commit()
|
||||||
|
# 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
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# print("invalid data type")
|
||||||
|
# return False
|
||||||
33
core/models/manage/manae_session.py
Normal file
33
core/models/manage/manae_session.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# db.py
|
||||||
|
from core.models.python_models.Base import Base
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
# Create engine
|
||||||
|
try:
|
||||||
|
engine = create_engine('sqlite:///data.db', echo=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to create engine: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
Session = scoped_session(sessionmaker(bind=engine))
|
||||||
|
|
||||||
|
def init_session():
|
||||||
|
"""Initialize database tables."""
|
||||||
|
try:
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Failed to initialize database: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
"""Get the thread-local session."""
|
||||||
|
return Session()
|
||||||
|
|
||||||
|
def close_session():
|
||||||
|
"""Clean up the thread-local session."""
|
||||||
|
try:
|
||||||
|
Session.remove()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing session: {e}")
|
||||||
27
core/models/manage/session_management.py
Normal file
27
core/models/manage/session_management.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from core.models.python_models.Base import Base
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///data.db")
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
_session = None
|
||||||
|
|
||||||
|
def init_session():
|
||||||
|
"""Call this once at application startup"""
|
||||||
|
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():
|
||||||
|
"""Call this when shutting down the application"""
|
||||||
|
global _session
|
||||||
|
if _session:
|
||||||
|
_session.close()
|
||||||
|
_session = None
|
||||||
19
core/models/manage/setup.py
Normal file
19
core/models/manage/setup.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///alchemy.db")
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_session():
|
||||||
|
session = Session()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
4
core/models/python_models/Base.py
Normal file
4
core/models/python_models/Base.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# base.py (or models/base.py)
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
48
core/models/python_models/CachedSync.py
Normal file
48
core/models/python_models/CachedSync.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
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.python_models.Base import Base
|
||||||
|
|
||||||
|
class CachedSync(Base):
|
||||||
|
__tablename__ = 'cached_sync'
|
||||||
|
|
||||||
|
# version of the cached sync itself 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)
|
||||||
|
|
||||||
|
# to use:
|
||||||
|
# Lookup by composite primary key
|
||||||
|
# record = session.get(Hello, (b_value))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # from core.models.Model import Model
|
||||||
|
# from dataclasses import dataclass
|
||||||
|
# from typing import Union, Optional
|
||||||
|
|
||||||
|
# _table_name: str = 'cached_sync'
|
||||||
|
|
||||||
|
# _table_definition: str = """
|
||||||
|
# 'version' int UNIQUE,
|
||||||
|
# 'applications' int,
|
||||||
|
# 'application_versions' int,
|
||||||
|
# 'operators' int,
|
||||||
|
# 'locations' int,
|
||||||
|
# 'subscriptions' int,
|
||||||
|
# """
|
||||||
|
|
||||||
|
# @dataclass
|
||||||
|
# class CachedSync(Model):
|
||||||
|
# version: int
|
||||||
|
# applications: int
|
||||||
|
# application_versions: int
|
||||||
|
# operators: int
|
||||||
|
# locations: int
|
||||||
|
# subscriptions: int
|
||||||
|
|
@ -212,3 +212,15 @@ class WebServiceApiService:
|
||||||
headers = None
|
headers = None
|
||||||
|
|
||||||
return requests.post(Constants.SP_API_BASE_URL + path, headers=headers, json=body, proxies=proxies)
|
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
|
||||||
|
|
@ -3,10 +3,10 @@ from core.Constants import Constants
|
||||||
|
|
||||||
# Prepare full endpoint urls:
|
# Prepare full endpoint urls:
|
||||||
def make_url(which_endpoint: str) -> str:
|
def make_url(which_endpoint: str) -> str:
|
||||||
domain = Constants.TICKET_API_BASE_URL
|
# domain = Constants.TICKET_API_BASE_URL
|
||||||
|
|
||||||
# hardcode option:
|
# hardcode option:
|
||||||
# domain = "https://ticket.hydraveil.net"
|
domain = 'https://dev-us-002.simplifiedprivacy.net/api/v1'
|
||||||
|
|
||||||
final_url = f"{domain}/{which_endpoint}"
|
final_url = f"{domain}/{which_endpoint}"
|
||||||
return final_url
|
return final_url
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ def regular_get_request(url: str, connection_observer: ConnectionObserver):
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print(f"we are doing a regular GET request to: {url}")
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
|
|
||||||
# Raises HTTPError for 4xx/5xx
|
# Raises HTTPError for 4xx/5xx
|
||||||
|
|
|
||||||
69
core/services/sync/compare_tables.py
Normal file
69
core/services/sync/compare_tables.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
|
||||||
|
def compare_tables(new_data: dict, old_table: dict) -> tuple:
|
||||||
|
|
||||||
|
type_of_new = type(new_data)
|
||||||
|
type_of_old = type(old_table)
|
||||||
|
print(type_of_new)
|
||||||
|
print(type_of_old)
|
||||||
|
|
||||||
|
# ============== FILTER ==================
|
||||||
|
new_is_dictionary = isinstance(new_data, dict)
|
||||||
|
old_is_dictionary = isinstance(old_table, dict)
|
||||||
|
if new_is_dictionary and old_is_dictionary:
|
||||||
|
print("Filter Passed. Both things are dictionaries.")
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# ============== PREPARE ==================
|
||||||
|
|
||||||
|
# what are the tables?
|
||||||
|
new_keys = new_data.keys()
|
||||||
|
old_keys = old_table.keys()
|
||||||
|
|
||||||
|
print(f"new_keys is {new_keys}")
|
||||||
|
print(f"old_keys is {old_keys}")
|
||||||
|
|
||||||
|
changed_tables = []
|
||||||
|
new_data_types = []
|
||||||
|
|
||||||
|
# ============== EVALUATE ==================
|
||||||
|
|
||||||
|
for new_key in new_keys:
|
||||||
|
print(f"This iteration's new_key is {new_key}")
|
||||||
|
each_value = new_data[new_key]
|
||||||
|
print(f"This value is {each_value}")
|
||||||
|
|
||||||
|
# is the VALUE we're evaluating even a number?
|
||||||
|
if not isinstance(each_value, int):
|
||||||
|
print("skipping, it's not an int")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# do we even know about this new table category?
|
||||||
|
if new_key not in old_keys:
|
||||||
|
new_data_types.append(new_key)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# we know about it, let's see if it's new:
|
||||||
|
if each_value > old_table[new_key]:
|
||||||
|
changed_tables.append(new_key)
|
||||||
|
|
||||||
|
return changed_tables, new_data_types
|
||||||
|
|
||||||
|
# ============== TESTING ==================
|
||||||
|
# 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}")
|
||||||
|
"""
|
||||||
184
core/services/sync/sync_service.py
Normal file
184
core/services/sync/sync_service.py
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
# 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.python_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
|
||||||
|
|
||||||
|
# Highest Level in this Flow:
|
||||||
|
def coordinate_cache_sync(
|
||||||
|
client_observer: Optional[ClientObserver] = None,
|
||||||
|
connection_observer: Optional[ConnectionObserver] = None
|
||||||
|
) -> dict:
|
||||||
|
|
||||||
|
# ==================== GET & VALIDATE DATA ============================
|
||||||
|
|
||||||
|
# get data from the endpoint:
|
||||||
|
new_data_payload = get_sync_cache_from_api(ClientObserver, ConnectionObserver)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(f"We have new data: {new_data_payload}")
|
||||||
|
print(type(new_data_payload))
|
||||||
|
print("")
|
||||||
|
|
||||||
|
# is is a dictionary?
|
||||||
|
if not isinstance(new_data_payload, dict):
|
||||||
|
return {"valid": False, "error_msg": "invalid_format_reply"}
|
||||||
|
|
||||||
|
if "data" not in new_data_payload:
|
||||||
|
return new_data_payload
|
||||||
|
|
||||||
|
new_data = new_data_payload.get("data", None)
|
||||||
|
|
||||||
|
# SPOOFING:
|
||||||
|
# new_data = {
|
||||||
|
# "version": 1,
|
||||||
|
# "applications": 1,
|
||||||
|
# "application_versions": 1,
|
||||||
|
# "client_version": 1,
|
||||||
|
# "operators": 1,
|
||||||
|
# "locations": 1,
|
||||||
|
# "subscriptions": 1
|
||||||
|
# }
|
||||||
|
|
||||||
|
if "version" not in new_data:
|
||||||
|
return new_data # it should have the error in there.
|
||||||
|
|
||||||
|
new_highest = new_data.get("version", None)
|
||||||
|
|
||||||
|
print(f"new highest version is {new_highest}")
|
||||||
|
|
||||||
|
if not new_highest: # version is empty
|
||||||
|
return new_data # it should have the error in there.
|
||||||
|
|
||||||
|
# ==================== INITIAL SQL SESSION =====================
|
||||||
|
session = get_session()
|
||||||
|
|
||||||
|
# Get the highest version already saved:
|
||||||
|
previous_highest = session.query(func.max(CachedSync.version)).scalar()
|
||||||
|
print(f"previous_highest is {previous_highest}")
|
||||||
|
|
||||||
|
# if this is the first sync, we have no data to compare it to,
|
||||||
|
if previous_highest is None:
|
||||||
|
print("No previous previous_highest or Previous data. Saving..")
|
||||||
|
|
||||||
|
# so save the data,
|
||||||
|
saved = save_data(new_data)
|
||||||
|
if saved:
|
||||||
|
print("Saved!")
|
||||||
|
|
||||||
|
# then the "changed" keys are all of the new ones,
|
||||||
|
changed_tables_keys = new_data.keys()
|
||||||
|
changed_tables = list(changed_tables_keys)
|
||||||
|
return changed_tables
|
||||||
|
|
||||||
|
# ==================== EVALUATE DATA =====================
|
||||||
|
|
||||||
|
print(f"previous_highest is {previous_highest}")
|
||||||
|
print(f"new_highest is {new_highest}")
|
||||||
|
|
||||||
|
# bail on the rest of this function if it's the same:
|
||||||
|
if previous_highest >= new_highest:
|
||||||
|
return {"result": "same"}
|
||||||
|
else:
|
||||||
|
print("The data is new! We now evaluate...")
|
||||||
|
|
||||||
|
# ==================== COMPARE DATA ============================
|
||||||
|
|
||||||
|
# we are only doing this if we actually have old data to compare it to.
|
||||||
|
query = {"version": previous_highest}
|
||||||
|
old_data_as_list = get_from_model(CachedSync, **query)
|
||||||
|
|
||||||
|
# if it's a list, get it out of there,
|
||||||
|
old_data = old_data_as_list[0]
|
||||||
|
|
||||||
|
print(f"old data is: {old_data}")
|
||||||
|
|
||||||
|
if old_data:
|
||||||
|
print(f"We got the old data successfully. Now comparing..")
|
||||||
|
|
||||||
|
# let's see which tables are NEW:
|
||||||
|
changed_tables, new_data_types = compare_tables(new_data, old_data)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(f"changed_tables is {changed_tables}")
|
||||||
|
print(f"new_data_types is {new_data_types}")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if new_data_types:
|
||||||
|
quantity_of_new_types = len(new_data_types)
|
||||||
|
else:
|
||||||
|
# we can't fetch the OLD data due to database error, so we need to replace it all,
|
||||||
|
print("database error. add logic here for quantity_of_new_types.")
|
||||||
|
changed_tables_keys = new_data.keys()
|
||||||
|
changed_tables = list(changed_tables_keys)
|
||||||
|
quantity_of_new_types = len(changed_tables)
|
||||||
|
|
||||||
|
# ==================== FINALLY =====================
|
||||||
|
return changed_tables
|
||||||
|
|
||||||
|
### Add error logging to this:
|
||||||
|
def save_data(data: dict) -> bool:
|
||||||
|
print(f"about to insert the NEW data into the database.. {data}")
|
||||||
|
try:
|
||||||
|
did_it_work = insert_into_model(CachedSync, data, True)
|
||||||
|
print(f"Did it insert? {did_it_work}")
|
||||||
|
except:
|
||||||
|
print("Error! We Failed to Insert the New Data!")
|
||||||
|
did_it_work = False
|
||||||
|
|
||||||
|
return did_it_work
|
||||||
|
|
||||||
|
|
||||||
|
def get_sync_cache_from_api(
|
||||||
|
client_observer: Optional[ClientObserver] = None,
|
||||||
|
connection_observer: Optional[ConnectionObserver] = None
|
||||||
|
) -> dict:
|
||||||
|
|
||||||
|
print("we are syncing with the API..")
|
||||||
|
|
||||||
|
# get the base url & verify it:
|
||||||
|
rejected_list = [None, False, ""]
|
||||||
|
# base_url = Constants.SP_API_BASE_URL
|
||||||
|
base_url = 'https://dev-us-002.simplifiedprivacy.net/api/v1' # temp spoof
|
||||||
|
if base_url in rejected_list:
|
||||||
|
# notification = "Base URL is Empty, so it can't fetch prices.."
|
||||||
|
# client_observer.notify(notification)
|
||||||
|
return {"valid": False, "error_code": "invalid_url"}
|
||||||
|
|
||||||
|
# combine with endpoint:
|
||||||
|
url = f"{base_url}/cachedsync"
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("getting data from server...")
|
||||||
|
sync_results = get_data_from_server(url, connection_observer)
|
||||||
|
|
||||||
|
if sync_results in rejected_list:
|
||||||
|
return {"valid": False, "error_code": "sync_failed"}
|
||||||
|
|
||||||
|
logger.debug(f"Inside the sync controller, sync_results is: {sync_results}")
|
||||||
|
except:
|
||||||
|
return {"valid": False, "error_code": "sync_failed"}
|
||||||
|
|
||||||
|
return sync_results
|
||||||
|
|
||||||
40
prototype_sync.py
Normal file
40
prototype_sync.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "sp-hydra-veil-core"
|
name = "sp-hydra-veil-core"
|
||||||
version = "2.3.4"
|
version = "2.3.5"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Simplified Privacy" },
|
{ name = "Simplified Privacy" },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue