Compare commits

...

8 commits

10 changed files with 127 additions and 90 deletions

View file

@ -13,12 +13,14 @@ from core.services.payment_phase.check_if_paid import _check_if_paid
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
from core.services.networking.send_data_to_server import send_data_to_server from core.services.networking.send_data_to_server import send_data_to_server
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
from core.utils.confirm_its_a_valid_key_choice import confirm_its_a_valid_key_choice # from core.utils.confirm_its_a_valid_key_choice import confirm_its_a_valid_key_choice
from core.services.helpers.valid_profile_quantity import valid_profile_quantity from core.services.helpers.valid_profile_quantity import valid_profile_quantity
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
from core.controllers.tickets.TicketSyncController import sync_ticket_prices from core.controllers.tickets.TicketSyncController import sync_ticket_prices
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
""" """
Inputs: Which plan (key), which crypto, and how many profiles Inputs: Which plan (key), which crypto, and how many profiles
@ -47,6 +49,12 @@ def initiate_payment(
invoice_data_object.add_error_code("already_exists") invoice_data_object.add_error_code("already_exists")
return invoice_data_object return invoice_data_object
billing_id = do_we_have_billing_id()
if billing_id:
invoice_data_object.add_error_code("billing_code_exists")
invoice_data_object.temp_billing_code = billing_id
return invoice_data_object
rejected_choices = [None, "", False] rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices: if how_many_profiles in rejected_choices:
@ -67,18 +75,6 @@ def initiate_payment(
invoice_data_object.add_error_code("no_keyplan") invoice_data_object.add_error_code("no_keyplan")
return invoice_data_object return invoice_data_object
# confirm the key choice is among the choices from their sync file,
# and if not, then sync again, and try the results from that new file,
is_valid_key = confirm_its_a_valid_key_choice(which_key, ticket_observer)
if not is_valid_key:
sync_results = sync_ticket_prices(ticket_observer, connection_observer)
second_try_to_match = confirm_its_a_valid_key_choice(
which_key, ticket_observer
)
if not second_try_to_match:
invoice_data_object.add_error_code("invalid_key")
return invoice_data_object
# get & save the public key: # get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local") public_key_results = get_pub_key(which_key, connection_observer, "local")

View file

@ -51,6 +51,9 @@ def prepare_tickets(
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"} return {"valid": False, "error_code": "failed_input"}
notification = "Preparing Cryptography Locally"
ticket_observer.notify("preparing", subject=notification)
# ok now we have the pre-reqs, let's use this high level orchestrator, # ok now we have the pre-reqs, let's use this high level orchestrator,
prep_results = ticket_prep_orchestrator( prep_results = ticket_prep_orchestrator(
how_many_profiles, ticket_observer, connection_observer how_many_profiles, ticket_observer, connection_observer

View file

@ -8,7 +8,6 @@ if TYPE_CHECKING:
from core.Constants import Constants from core.Constants import Constants
from core.observers.BaseObserver import BaseObserver from core.observers.BaseObserver import BaseObserver
from core.services.networking.get_data_from_server import get_data_from_server from core.services.networking.get_data_from_server import get_data_from_server
from core.services.helpers.save_sync_results import save_sync_results
from core.errors.logger import logger from core.errors.logger import logger
@ -38,7 +37,4 @@ def sync_ticket_prices(
except: except:
return {"valid": False, "error_code": "sync_failed"} return {"valid": False, "error_code": "sync_failed"}
did_it_save = save_sync_results(sync_results)
logger.debug(f"Inside the sync controller, did_it_save is {did_it_save}")
return sync_results return sync_results

View file

@ -14,7 +14,9 @@ class SystemProfile(BaseProfile):
connection: Optional[SystemConnection] connection: Optional[SystemConnection]
def get_system_config_path(self): def get_system_config_path(self):
return self.__get_system_config_path(self.id) filepath = self.__get_system_config_path(self.id)
the_id = self.id
return filepath
def save(self): def save(self):
@ -48,10 +50,15 @@ class SystemProfile(BaseProfile):
raise ProfileModificationError('The WireGuard configuration could not be attached.') raise ProfileModificationError('The WireGuard configuration could not be attached.')
def get_wireguard_configuration_path(self): def get_wireguard_configuration_path(self):
return f'{self.get_system_config_path()}/wg.conf' filepath = f'{self.get_system_config_path()}/wg.conf'
return filepath
def has_wireguard_configuration(self): def has_wireguard_configuration(self):
return os.path.isfile(f'{self.get_system_config_path()}/wg.conf') filepath = f'{self.get_system_config_path()}/wg.conf'
if os.path.isdir(os.path.dirname(filepath)):
return os.path.isfile(filepath)
else:
return False
def address_security_incident(self): def address_security_incident(self):
@ -59,7 +66,6 @@ class SystemProfile(BaseProfile):
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
def delete(self): def delete(self):
try: try:
self.__delete_wireguard_configuration() self.__delete_wireguard_configuration()
except ProfileModificationError: except ProfileModificationError:
@ -68,11 +74,13 @@ class SystemProfile(BaseProfile):
if shutil.which('pkexec') is None: if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec') raise CommandNotFoundError('pkexec')
process = subprocess.Popen(('pkexec', 'rm', '-d', self.get_system_config_path())) try:
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) process = subprocess.run(('pkexec', 'rm', '-rf', self.get_system_config_path()))
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
if not completed_successfully: if not completed_successfully:
raise ProfileDeletionError('The profile could not be deleted.') raise ProfileDeletionError('The profile could not be deleted.')
except:
print("skipping the delete of the WG folder.")
super().delete() super().delete()
@ -83,12 +91,19 @@ class SystemProfile(BaseProfile):
if shutil.which('pkexec') is None: if shutil.which('pkexec') is None:
raise CommandNotFoundError('pkexec') raise CommandNotFoundError('pkexec')
process = subprocess.Popen(('pkexec', 'rm', '-d', self.get_wireguard_configuration_path())) try:
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8) process = subprocess.run(('pkexec', 'rm', '-rf', self.get_wireguard_configuration_path()), check=True)
completed_successfully = not bool(os.waitpid(process.pid, 0)[1] >> 8)
except subprocess.CalledProcessError as e:
completed_successfully = True
except:
completed_successfully = True
if not completed_successfully: if not completed_successfully:
raise ProfileModificationError('The WireGuard configuration could not be deleted.') raise ProfileModificationError('The WireGuard configuration could not be deleted.')
@staticmethod @staticmethod
def __get_system_config_path(id: int): def __get_system_config_path(id: int):
return f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}' config_path = f'{Constants.HV_SYSTEM_PROFILE_CONFIG_PATH}/{str(id)}'
return config_path

View file

@ -2,6 +2,7 @@ from core.Constants import Constants
from core.models.ClientVersion import ClientVersion from core.models.ClientVersion import ClientVersion
from core.models.Location import Location from core.models.Location import Location
from core.models.Operator import Operator from core.models.Operator import Operator
from core.models.OperatorProxySession import OperatorProxySession
from core.models.Subscription import Subscription from core.models.Subscription import Subscription
from core.models.SubscriptionPlan import SubscriptionPlan from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.invoice.Invoice import Invoice from core.models.invoice.Invoice import Invoice
@ -18,12 +19,10 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_applications(proxies: Optional[dict] = None): def get_applications(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/platforms/linux-x86_64/applications', None, proxies) response = WebServiceApiService.__get('/platforms/linux-x86_64/applications', None, proxies)
applications = [] applications = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for application in response.json()['data']: for application in response.json()['data']:
applications.append(Application(application['code'], application['name'], application['id'])) applications.append(Application(application['code'], application['name'], application['id']))
@ -32,12 +31,10 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_application_versions(code: str, proxies: Optional[dict] = None): def get_application_versions(code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get(f'/platforms/linux-x86_64/applications/{code}/application-versions', None, proxies) response = WebServiceApiService.__get(f'/platforms/linux-x86_64/applications/{code}/application-versions', None, proxies)
application_versions = [] application_versions = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for application_version in response.json()['data']: for application_version in response.json()['data']:
application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['format_revision'], application_version['id'], application_version['download_path'], application_version['released_at'], application_version['file_hash'])) application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['format_revision'], application_version['id'], application_version['download_path'], application_version['released_at'], application_version['file_hash']))
@ -46,12 +43,10 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_client_versions(proxies: Optional[dict] = None): def get_client_versions(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/platforms/linux-x86_64/appimage/client-versions', None, proxies) response = WebServiceApiService.__get('/platforms/linux-x86_64/appimage/client-versions', None, proxies)
client_versions = [] client_versions = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for client_version in response.json()['data']: for client_version in response.json()['data']:
client_versions.append(ClientVersion(client_version['version_number'], client_version['released_at'], client_version['id'], client_version['download_path'])) client_versions.append(ClientVersion(client_version['version_number'], client_version['released_at'], client_version['id'], client_version['download_path']))
@ -60,26 +55,22 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_operators(proxies: Optional[dict] = None): def get_operators(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/operators', None, proxies) response = WebServiceApiService.__get('/operators', None, proxies)
operators = [] operators = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for operator in response.json()['data']: for operator in response.json()['data']:
operators.append(Operator(operator['id'], operator['name'], operator['public_key'], operator['nostr_public_key'], operator['nostr_profile_reference'], operator['nostr_attestation']['event_reference'])) operators.append(Operator(operator['id'], operator['name'], operator['type'], operator['public_key'], operator['nostr_public_key'], operator['nostr_profile_reference'], operator['nostr_attestation']['event_reference']))
return operators return operators
@staticmethod @staticmethod
def get_locations(proxies: Optional[dict] = None): def get_locations(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/locations', None, proxies) response = WebServiceApiService.__get('/locations', None, proxies)
locations = [] locations = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for location in response.json()['data']: for location in response.json()['data']:
locations.append(Location(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'])) locations.append(Location(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']))
@ -88,60 +79,57 @@ class WebServiceApiService:
@staticmethod @staticmethod
def get_subscription_plans(proxies: Optional[dict] = None): def get_subscription_plans(proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/subscription-plans', None, proxies) response = WebServiceApiService.__get('/subscription-plans', None, proxies)
subscription_plans = [] subscription_plans = []
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
for subscription_plan in response.json()['data']: for subscription_plan in response.json()['data']:
subscription_plans.append(SubscriptionPlan(subscription_plan['id'], subscription_plan['code'], subscription_plan['wireguard_session_limit'], subscription_plan['duration'], subscription_plan['price'], subscription_plan['features_proxy'], subscription_plan['features_wireguard'])) subscription_plans.append(SubscriptionPlan(subscription_plan['id'], subscription_plan['code'], subscription_plan['wireguard_session_limit'], subscription_plan['duration'], subscription_plan['price'], subscription_plan['features_proxy'], subscription_plan['features_wireguard']))
return subscription_plans return subscription_plans
@staticmethod @staticmethod
def post_subscription(subscription_plan_id, location_id, proxies: Optional[dict] = None): def post_subscription(subscription_plan_id, location_id=None, operator_id=None, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes body = {'subscription_plan_id': subscription_plan_id}
response = WebServiceApiService.__post('/subscriptions', None, {
'subscription_plan_id': subscription_plan_id,
'location_id': location_id
}, proxies)
if response.status_code == status_codes.CREATED:
return Subscription(response.headers['X-Billing-Code'])
if operator_id is not None:
body['operator_id'] = operator_id
else: else:
return None body['location_id'] = location_id
response = WebServiceApiService.__post('/subscriptions', None, body, proxies)
if 200 <= response.status_code < 300:
return Subscription(response.headers['X-Billing-Code'], operator_id=operator_id)
return None
@staticmethod @staticmethod
def get_subscription(billing_code: str, proxies: Optional[dict] = None): def get_subscription(billing_code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
billing_code = billing_code.replace('-', '').upper() billing_code = billing_code.replace('-', '').upper()
billing_code_fragments = re.findall('....?', billing_code) billing_code_fragments = re.findall('....?', billing_code)
billing_code = '-'.join(billing_code_fragments) billing_code = '-'.join(billing_code_fragments)
response = WebServiceApiService.__get('/subscriptions/current', billing_code, proxies) response = WebServiceApiService.__get('/subscriptions/current', billing_code, proxies)
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
subscription = response.json()['data'] subscription = response.json()['data']
return Subscription(billing_code, Subscription.from_iso_format(subscription['expires_at'])) return Subscription(
billing_code,
operator_id=subscription.get('operator_id'),
expires_at=Subscription.from_iso_format(subscription['expires_at'])
)
else: return None
return None
@staticmethod @staticmethod
def get_invoice(billing_code: str, proxies: Optional[dict] = None): def get_invoice(billing_code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/invoices/current', billing_code, proxies) response = WebServiceApiService.__get('/invoices/current', billing_code, proxies)
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
response_data = response.json()['data'] response_data = response.json()['data']
@ -157,37 +145,54 @@ class WebServiceApiService:
return Invoice(billing_code, invoice['status'], invoice['expires_at'], tuple[PaymentMethod](payment_methods)) return Invoice(billing_code, invoice['status'], invoice['expires_at'], tuple[PaymentMethod](payment_methods))
else: return None
return None
@staticmethod @staticmethod
def get_proxy_configuration(billing_code: str, proxies: Optional[dict] = None): def get_proxy_configuration(billing_code: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__get('/proxy-configurations/current', billing_code, proxies) response = WebServiceApiService.__get('/proxy-configurations/current', billing_code, proxies)
if response.status_code == status_codes.OK: if 200 <= response.status_code < 300:
proxy_configuration = response.json()['data'] proxy_configuration = response.json()['data']
return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'], proxy_configuration['location']['time_zone']['code']) return ProxyConfiguration(proxy_configuration['ip_address'], proxy_configuration['port'], proxy_configuration['username'], proxy_configuration['password'], proxy_configuration['location']['time_zone']['code'])
else: return None
return None
@staticmethod
def post_operator_proxy(billing_code: str, operator_id: int, protocol: str, proxies: Optional[dict] = None):
response = WebServiceApiService.__post('/subscriptions/current/operator-proxies', billing_code, {
'operator_id': operator_id,
'protocol': protocol,
}, proxies)
if 200 <= response.status_code < 300:
data = response.json()['data']
return OperatorProxySession(
data['id'],
data['type'],
data['username'],
data.get('password'),
data.get('links'),
data.get('subscription_url'),
data['operator']['id'],
data['operator']['name'],
data['operator'].get('domain'),
)
return None
@staticmethod @staticmethod
def post_wireguard_session(country_code: str, location_code: str, billing_code: str, public_key: str, proxies: Optional[dict] = None): def post_wireguard_session(country_code: str, location_code: str, billing_code: str, public_key: str, proxies: Optional[dict] = None):
from requests.status_codes import codes as status_codes
response = WebServiceApiService.__post(f'/countries/{country_code}/locations/{location_code}/wireguard-sessions', billing_code, { response = WebServiceApiService.__post(f'/countries/{country_code}/locations/{location_code}/wireguard-sessions', billing_code, {
'public_key': public_key, 'public_key': public_key,
}, proxies) }, proxies)
if response.status_code == status_codes.CREATED: if 200 <= response.status_code < 300:
return response.text return response.text
else:
return None return None
@staticmethod @staticmethod
def __get(path, billing_code: Optional[str] = None, proxies: Optional[dict] = None): def __get(path, billing_code: Optional[str] = None, proxies: Optional[dict] = None):
@ -211,4 +216,4 @@ class WebServiceApiService:
else: else:
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)

View file

@ -96,7 +96,7 @@ def is_the_key_to_blame(
# invalid key # invalid key
notification = f"Invalid Numbers on the public_key" notification = f"Invalid Numbers on the public_key"
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return {"valid": True, "message": "invalid_key"} return {"valid": False, "message": "invalid_key"}
new_public_key = get_new_pubkey_from_api(connection_observer) new_public_key = get_new_pubkey_from_api(connection_observer)
@ -121,7 +121,7 @@ def is_the_key_to_blame(
if not result_of_comparison: if not result_of_comparison:
error_msg = "New key is the SAME as the old one." error_msg = "New key is the SAME as the old one."
ticket_observer.notify("preparing", subject=error_msg) ticket_observer.notify("preparing", subject=error_msg)
return {"valid": False, "message": "same"} return {"valid": False, "comparison": "same"}
status_update = "New key is DIFFERENT from the old one!" status_update = "New key is DIFFERENT from the old one!"
ticket_observer.notify("preparing", subject=status_update) ticket_observer.notify("preparing", subject=status_update)
@ -142,9 +142,9 @@ def is_the_key_to_blame(
if quantity_results > 0: if quantity_results > 0:
logger.debug("Therefore, the new key works.") logger.debug("Therefore, the new key works.")
return {"valid": True, "comparison": "different", "matters": False} return {"valid": True, "comparison": "different"}
else: else:
logger.debug( logger.debug(
"Therefore, the new key doesn't help. It is different, but also produces invalid blind signatures." "Therefore, the new key doesn't help. It is different, but also produces invalid blind signatures."
) )
return {"valid": False, "comparison": "different", "matters": False} return {"valid": False, "comparison": "different"}

View file

@ -0,0 +1,13 @@
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
from core.Constants import Constants
def do_we_have_billing_id() -> str | None:
try:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json"
billing_id = get_value_from_json_file(filepath, "temp_billing_code")
return billing_id
except:
return None

View file

@ -37,7 +37,7 @@ def get_from_server_and_save(
if status == True: if status == True:
# extract: # extract:
public_key = public_key_results.get("valid", False) public_key = public_key_results.get("data", False)
# save it: # save it:
did_it_save = write_string_to_text_file(public_key, file_path) did_it_save = write_string_to_text_file(public_key, file_path)

View file

@ -37,6 +37,8 @@ def ticket_prep_orchestrator(
} }
# assuming we actually saved the unblinding factors, # assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.."
ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign: # then send the entire blinded list to the server to sign:
blind_signatures = send_blind_commitments( blind_signatures = send_blind_commitments(
@ -56,9 +58,13 @@ def ticket_prep_orchestrator(
else: else:
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again, # regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again,
notification = "Saving the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures) did_they_ALL_save = save_ALL_blind_sigs(blind_signatures)
# verify the server's blind signatures against the public key, # verify the server's blind signatures against the public key,
notification = "Evaluating the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
failed_validations = validate_blind_signatures( failed_validations = validate_blind_signatures(
blind_signatures, ticket_observer, connection_observer blind_signatures, ticket_observer, connection_observer
) )
@ -68,6 +74,8 @@ def ticket_prep_orchestrator(
# did verification of any of the blind sigs fail? # did verification of any of the blind sigs fail?
how_many_failed = len(failed_validations) how_many_failed = len(failed_validations)
if how_many_failed >= 1: if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures."
ticket_observer.notify("preparing", subject=notification)
logger.debug( logger.debug(
f"Verification failed for {how_many_failed} blind signatures." f"Verification failed for {how_many_failed} blind signatures."
) )
@ -79,6 +87,8 @@ def ticket_prep_orchestrator(
} }
# Unblind the signatures & combine with unblinded commitment: # Unblind the signatures & combine with unblinded commitment:
notification = f"Unblinding Signatures & Preparing Tickets..."
ticket_observer.notify("preparing", subject=notification)
did_prep_work = unblind_ALL_tickets( did_prep_work = unblind_ALL_tickets(
blind_signatures, ticket_observer, connection_observer blind_signatures, ticket_observer, connection_observer
) )

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-core" name = "sp-hydra-veil-core"
version = "2.3.0" version = "2.3.4"
authors = [ authors = [
{ name = "Simplified Privacy" }, { name = "Simplified Privacy" },
] ]
@ -15,7 +15,6 @@ dependencies = [
"cryptography ~= 46.0.3", "cryptography ~= 46.0.3",
"dataclasses-json ~= 0.6.7", "dataclasses-json ~= 0.6.7",
"marshmallow ~= 3.26.1", "marshmallow ~= 3.26.1",
"psutil ~= 7.1.3",
"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",