Compare commits

...
13 Commits
13 changed files with 165 additions and 74 deletions
+2 -1
View File
@@ -30,7 +30,8 @@ jobs:
pipenv install --system
- name: Run pipenv check
run: |
pipenv check
pipenv check \
--ignore 51499 # DDoS attack in wheel package, which is unsupported in python 3.7
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Generated
+39 -39
View File
@@ -16,18 +16,18 @@
"default": {
"arrow": {
"hashes": [
"sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b",
"sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"
"sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1",
"sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"
],
"index": "pypi",
"version": "==1.2.2"
"version": "==1.2.3"
},
"beautifulsoup4": {
"hashes": [
"sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30",
"sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"
],
"markers": "python_full_version >= '3.6.0'",
"markers": "python_version >= '3.6'",
"version": "==4.11.1"
},
"bs4": {
@@ -39,27 +39,27 @@
},
"certifi": {
"hashes": [
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
"sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
"sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
],
"markers": "python_full_version >= '3.6.0'",
"version": "==2022.6.15"
"markers": "python_version >= '3.6'",
"version": "==2022.9.24"
},
"charset-normalizer": {
"hashes": [
"sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5",
"sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
],
"markers": "python_full_version >= '3.6.0'",
"version": "==2.1.0"
"markers": "python_version >= '3.6'",
"version": "==2.1.1"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.3"
"version": "==3.4"
},
"lxml": {
"hashes": [
@@ -156,11 +156,11 @@
},
"python-dotenv": {
"hashes": [
"sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f",
"sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"
"sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5",
"sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"
],
"index": "pypi",
"version": "==0.20.0"
"version": "==0.21.0"
},
"requests": {
"hashes": [
@@ -172,11 +172,11 @@
},
"requests-mock": {
"hashes": [
"sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970",
"sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"
"sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699",
"sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b"
],
"index": "pypi",
"version": "==1.9.3"
"version": "==1.10.0"
},
"six": {
"hashes": [
@@ -191,20 +191,20 @@
"sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
"sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
],
"markers": "python_full_version >= '3.6.0'",
"markers": "python_version >= '3.6'",
"version": "==2.3.2.post1"
},
"tconnectsync": {
"path": ".",
"version": "==0.7.1"
"version": "==0.8.6"
},
"urllib3": {
"hashes": [
"sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec",
"sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"
"sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
"version": "==1.26.10"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.13"
}
},
"develop": {
@@ -217,11 +217,11 @@
},
"jedi": {
"hashes": [
"sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d",
"sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"
"sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e",
"sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"
],
"markers": "python_version >= '3.6'",
"version": "==0.18.1"
"version": "==0.18.2"
},
"parso": {
"hashes": [
@@ -233,27 +233,27 @@
},
"prompt-toolkit": {
"hashes": [
"sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0",
"sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"
"sha256:535c29c31216c77302877d5120aef6c94ff573748a5b5ca5b1b1f76f5e700c73",
"sha256:ced598b222f6f4029c0800cefaa6a17373fb580cd093223003475ce32805c35b"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.0.30"
"version": "==3.0.33"
},
"ptpython": {
"hashes": [
"sha256:99636899ab0e4d026e2ecc9368269114f387b4bb5411e57f072b0bde724d9f99",
"sha256:eafd4ced27ca5dc370881d4358d1ab5041b32d88d31af8e3c24167fe4af64ed6"
"sha256:1ec28ba37959e8e6c4880fcf4748ad5c61341d4c1bfd24a38a4d5b97af1f475a",
"sha256:a57b9952eac4a12540a4dfb4ccd4a243403eceb27b0d13245b5e4144c8731d32"
],
"index": "pypi",
"version": "==3.0.20"
"version": "==3.0.21"
},
"pygments": {
"hashes": [
"sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb",
"sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"
"sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1",
"sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"
],
"markers": "python_version >= '3.6'",
"version": "==2.12.0"
"version": "==2.13.0"
},
"wcwidth": {
"hashes": [
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = tconnectsync
version = 0.8.5
version = 0.8.10
author = James Woglom
author_email = j@wogloms.net
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
+9 -3
View File
@@ -55,10 +55,16 @@ class ControlIQApi:
self.tconnect_software_ver = version
logger.info("Reported tconnect software version: %s" % version)
if version != self.LAST_CONFIRMED_SOFTWARE_VERSION:
logger.warn("Newer API version than last confirmed working. Saw %s and expected %s" % (version, self.LAST_CONFIRMED_SOFTWARE_VERSION))
logger.warn("If you experience any issues, please report them to https://github.com/jwoglom/tconnectsync")
logger.warning("Newer API version than last confirmed working. Saw %s and expected %s" % (version, self.LAST_CONFIRMED_SOFTWARE_VERSION))
logger.warning("If you experience any issues, please report them to https://github.com/jwoglom/tconnectsync")
except Exception:
logger.warn("Unable to find tconnect software version")
logger.warning("Unable to find tconnect software version.")
contents = "<unknown>"
if soup:
contents = "%s" % soup.encode_contents()
if len(contents) > 1000:
contents = "%s[SNIP]%s" % (contents[:500], contents[-500:])
logger.info("BeautifulSoup parsed contents: %s" % contents)
pass
return {
"__LASTFOCUS": "",
+3 -3
View File
@@ -76,7 +76,7 @@ class Autoupdate:
logger.error("Exiting with error code due to AUTOUPDATE_RESTART_ON_FAILURE")
return 1
else:
logger.warn(AutoupdateFailureWarning(("%s: An event index change was recorded, but no new data was found via the API. " % datetime.datetime.now()) +
logger.warning(AutoupdateFailureWarning(("%s: An event index change was recorded, but no new data was found via the API. " % datetime.datetime.now()) +
"The %s was %d minutes ago. Resetting TConnectApi to attempt to solve this problem." %
("last processed event" if self.last_successful_process_time_range else "start of autoupdate", (now - last_action_or_start)//60)))
@@ -143,7 +143,7 @@ class Autoupdate:
# uploaded, so
if len(self.time_diffs_between_attempts) >= 3:
# The pump hasn't sent us data that, based on previous cadence, we were expecting
logger.warn(AutoupdateNoIndexChangeWarning("Sleeping %d seconds after unexpected no index change based on previous cadence. (New data might be delayed.)" %
logger.warning(AutoupdateNoIndexChangeWarning("Sleeping %d seconds after unexpected no index change based on previous cadence. (New data might be delayed.)" %
int(self.secret.AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS)))
logger.debug("Last event time: %s, time diffs between attempts: %s" % (self.last_event_time, self.time_diffs_between_attempts))
@@ -204,4 +204,4 @@ class AutoupdateNoNewDataDetectedError(AutoupdateError):
pass
class AutoupdateNoIndexChangeWarning(AutoupdateWarning):
pass
pass
+24 -10
View File
@@ -2,9 +2,10 @@ import sys
import time
import arrow
import logging
import traceback
import pkg_resources
from datetime import datetime
from pprint import pformat
from pprint import pformat as pformat_base
from .nightscout import NightscoutApi
from .parser.nightscout import BASAL_EVENTTYPE, BOLUS_EVENTTYPE
@@ -29,6 +30,13 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
print(*args)
loglines.append(" ".join([str(i) for i in args]) + "\n")
def log_err(e):
try:
out = ''.join(list(traceback.TracebackException.from_exception(e).format()))
log(out)
except Exception:
log("could not log exception traceback: {}".format(e))
def debug(*args):
if verbose:
print(*args)
@@ -46,7 +54,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
from .secret import TCONNECT_EMAIL, TCONNECT_PASSWORD, PUMP_SERIAL_NUMBER, NS_URL, NS_SECRET, TIMEZONE_NAME
except ImportError as e:
log("Error: Unable to load config file. Please check your .env file or environment variables")
log(e)
log_err(e)
if not TCONNECT_EMAIL or TCONNECT_EMAIL == 'email@email.com':
log("Error: You have not specified a TCONNECT_EMAIL")
@@ -79,7 +87,9 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
log("tconnect_software_ver: %s" % tconnect.controliq.tconnect_software_ver)
except Exception as e:
log("Error occurred querying ControlIQ API for dashboard_summary:")
log(e)
log_err(e)
if e and "HTTP 404" in str(e):
log("<!> The API returns a 404 error if there is no data for the provided dates (currently %s - %s). Try re-running with an earlier start date using --start-date and --end-date arguments." % (time_start, time_end))
errors += 1
log("Querying ControlIQ therapy_timeline...")
@@ -97,7 +107,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
lastBasalDuration = processed_tt[-1]['duration_mins']
except Exception as e:
log("Error occurred querying ControlIQ therapy_timeline:")
log(e)
log_err(e)
errors += 1
log("Querying ControlIQ therapy_events...")
@@ -106,7 +116,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
debug("controliq therapy_events: \n%s" % pformat(androidevents))
except Exception as e:
log("Error occurred querying ControlIQ therapy_events:")
log(e)
log_err(e)
errors += 1
log("-----")
@@ -119,7 +129,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
ws2_loggedin = True
except Exception as e:
log("Error occurred querying WS2 API. This is okay so long as you are not using the PUMP_EVENTS or IOB sync features.")
log(e)
log_err(e)
errors += 1
lastReadingTime = None
@@ -133,7 +143,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
lastReadingTime = TConnectEntry._datetime_parse(ttcsv["readingData"][-1]['EventDateTime'])
except Exception as e:
log("Error occurred querying WS2 therapy_timeline_csv. This is okay so long as you are not using the PUMP_EVENTS or IOB sync features.")
log(e)
log_err(e)
errors += 1
else:
log("Not able to log in to WS2 API, so skipping therapy_timeline_csv")
@@ -150,7 +160,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
debug("Android last uploaded event: \n%s" % pformat(event))
except Exception as e:
log("Error occurred querying Android API:")
log(e)
log_err(e)
errors += 1
log("-----")
@@ -168,7 +178,7 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
debug("Nightscout last uploaded bolus: \n%s" % pformat(last_upload_bolus))
except Exception as e:
log("Error occurred querying Nightscout API:")
log(e)
log_err(e)
errors += 1
log("-----")
@@ -222,4 +232,8 @@ def run_sanitize(s, sanitizedData):
for k, v in sanitizedData.items():
if v and len(str(v)) > 0:
ret = ret.replace(str(v), '[%s]' % k)
return ret
return ret
def pformat(*args, **kwargs):
kwargs['width'] = 160
return pformat_base(*args, **kwargs)
+49
View File
@@ -429,3 +429,52 @@ class BolusTherapyEvent(TherapyEvent):
'type': 'BG',
'uploadId': 0}
"""
class BasalTherapyEvent(TherapyEvent):
"""
{
'basalRate': {
'duration': 0,
'percent': 0,
'value': 0.0
},
'displayInHistory': 0,
'eventDateTime': '2022-12-02T00:00:00',
'note': {
'id': 0,
'indexId': '16403',
'eventTypeId': 90,
'sourceRecordId': 0,
'eventId': 0,
'active': False
},
'noteDate': {},
'requestDateTime': '0001-01-01T00:00:00',
'type': 'Basal',
'description': 'NDE',
'sourceRecId': xxx,
'eventTypeId': 0,
'indexId': 0,
'uploadId': 0,
'interactive': 1,
'tempRateId': 0,
'tempRateCompleted': 0,
'tempRateActivated': 0
}
"""
basalRateValue = None
basalRatePercent = None
basalRateDuration = None
eventTime = None
@classmethod
def parse(_, json):
self = CGMTherapyEvent()
TherapyEvent.parse(self, json)
if 'basalRate' in json:
self.basalRateValue = json['basalRate']['value']
self.basalRatePercent = json['basalRate']['percent']
self.basalRateDuration = json['basalRate']['duration']
self.eventTime = json['eventDateTime']
return self
+11 -2
View File
@@ -77,9 +77,18 @@ class NightscoutApi:
return j[0]
return None
ret = internal(False)
ret = None
try:
ret = internal(False)
except ApiException as e:
logger.warning("last_uploaded_entry with no t_to_space: %s", e)
ret = None
if ret is None and (time_start or time_end):
ret = internal(True)
try:
ret = internal(True)
except ApiException as e:
logger.warning("last_uploaded_entry with t_to_space: %s", e)
ret = None
if ret is not None:
logger.warning("last_uploaded_entry with eventType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (eventType, time_start, time_end))
return ret
+6 -2
View File
@@ -1,4 +1,4 @@
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent, BasalTherapyEvent
from tconnectsync.parser.tconnect import TConnectEntry
import logging
@@ -9,6 +9,7 @@ def split_therapy_events(ciqTherapyEvents):
bolusEvents = []
cgmEvents = []
bgEvents = []
basalEvents = []
for e in ciqTherapyEvents['event']:
event = TConnectEntry.parse_therapy_event(e)
if isinstance(event, BolusTherapyEvent):
@@ -17,7 +18,10 @@ def split_therapy_events(ciqTherapyEvents):
cgmEvents.append(event)
elif isinstance(event, BGTherapyEvent):
bgEvents.append(event)
elif isinstance(event, BasalTherapyEvent):
basalEvents.append(event)
logger.debug("split_therapy_events: %d bolus, %d CGM, %d BG" % (len(bolusEvents), len(cgmEvents), len(bgEvents)))
logger.debug("split_therapy_events: %d bolus, %d CGM, %d BG, %d basal" % (len(bolusEvents), len(cgmEvents), len(bgEvents), len(basalEvents)))
# TODO: BG events (CGM Calibration) values are not currently returned from ciq_therapy_events.py
return bolusEvents, cgmEvents
+3 -1
View File
@@ -3,7 +3,7 @@ import sys
import arrow
from tconnectsync.domain.bolus import Bolus
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent, BasalTherapyEvent
try:
from ..secret import TIMEZONE_NAME
@@ -202,6 +202,8 @@ class TConnectEntry:
return CGMTherapyEvent.parse(data)
elif data["type"] == "BG":
return BGTherapyEvent.parse(data)
elif data["type"] == "Basal":
return BasalTherapyEvent.parse(data)
raise UnknownTherapyEventException(data)
+10 -10
View File
@@ -86,19 +86,19 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
ciqFallingBack or \
BOLUS_BG in features or \
IOB in features:
logger.warn("Downloading t:connect CSV data")
logger.warning("Downloading t:connect CSV data")
if bolusFallingBack:
logger.warn("Falling back on WS2 CSV data source because BOLUS is an enabled feature and CIQ bolus data was empty!!")
logger.warning("Falling back on WS2 CSV data source because BOLUS is an enabled feature and CIQ bolus data was empty!!")
if ciqFallingBack:
logger.warn("Falling back on WS2 CSV data source because CGM is an enabled feature and CIQ cgm data was empty!!")
logger.warning("Falling back on WS2 CSV data source because CGM is an enabled feature and CIQ cgm data was empty!!")
if BOLUS_BG in features:
logger.warn("Falling back on WS2 CSV data source because BOLUS_BG is an enabled feature. " +
logger.warning("Falling back on WS2 CSV data source because BOLUS_BG is an enabled feature. " +
"Please consider disabling this feature to improve synchronization reliability.")
if IOB in features:
logger.warn("Falling back on WS2 CSV data source because IOB is an enabled feature. " +
logger.warning("Falling back on WS2 CSV data source because IOB is an enabled feature. " +
"Please consider disabling this feature to improve synchronization reliability.")
logger.warn("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
logger.warning("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
csvdata = tconnect.ws2.therapy_timeline_csv(time_start, time_end)
csvReadingData = csvdata["readingData"]
@@ -147,8 +147,8 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
pumpEvents = process_ciq_activity_events(ciqTherapyTimelineData)
logger.debug("CIQ activity events: %s" % pumpEvents)
logger.warn("Using WS2 data source for basalsuspension because PUMP_EVENTS is an enabled feature")
logger.warn("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
logger.warning("Using WS2 data source for basalsuspension because PUMP_EVENTS is an enabled feature")
logger.warning("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
ws2BasalSuspension = tconnect.ws2.basalsuspension(time_start, time_end)
bsPumpEvents = process_basalsuspension_events(ws2BasalSuspension)
@@ -167,7 +167,7 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
bolusEvents = process_bolus_events(ciqBolusData, source="ciq")
if csvBolusData and not bolusEvents:
logger.warn("Falling back on non-CIQ csvBolusData")
logger.warning("Falling back on non-CIQ csvBolusData")
bolusEvents = process_bolus_events(csvBolusData, source="csv")
logger.debug("ciq bolusEvents: %s" % bolusEvents)
@@ -184,4 +184,4 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
logger.debug("Finished writing iob events")
logger.info("Wrote %d events to Nightscout this process cycle" % added)
return added
return added
+6
View File
@@ -34,6 +34,12 @@ def process_bolus_events(bolusdata, cgmEvents=None, source=""):
else:
logger.warning("Skipping non-completed %s bolus data (was a bolus in progress?): %s parsed: %s" % (source, b, parsed))
continue
if parsed.is_extended_bolus:
if not parsed.bolex_start_time and not parsed.start_time:
logger.warning("Skipping non-completed %s extended bolus data with no start time: %s parsed: %s" % (source, b, parsed))
elif not parsed.bolex_start_time and parsed.start_time:
logger.warning("Setting bolex_start_time to start_time for non-completed %s extended bolus: %s parsed: %s" % (source, b, parsed))
parsed.bolex_start_time = parsed.start_time
if parsed.bg and cgmEvents:
requested_at = parsed.request_time if not parsed.extended_bolus else parsed.bolex_start_time
+2 -2
View File
@@ -24,9 +24,9 @@ def build_mock_logger():
logger.error(*args, **kwargs)
def fake_warn(*args, **kwargs):
logger.warn(*args, **kwargs)
logger.warning(*args, **kwargs)
with patch("tconnectsync.autoupdate.logger.error") as mock_error, patch("tconnectsync.autoupdate.logger.warn") as mock_warn:
with patch("tconnectsync.autoupdate.logger.error") as mock_error, patch("tconnectsync.autoupdate.logger.warning") as mock_warn:
mock_error.side_effect = fake_error
mock_warn.side_effect = fake_warn
yield (mock_error, mock_warn)