sfq 0.0.9__py3-none-any.whl → 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sfq/__init__.py
CHANGED
@@ -1,14 +1,19 @@
|
|
1
|
+
import base64
|
1
2
|
import http.client
|
2
3
|
import json
|
3
4
|
import logging
|
4
5
|
import os
|
5
6
|
import time
|
7
|
+
import warnings
|
8
|
+
from queue import Empty, Queue
|
6
9
|
from typing import Any, Dict, Optional
|
7
10
|
from urllib.parse import quote, urlparse
|
8
11
|
|
9
12
|
TRACE = 5
|
10
13
|
logging.addLevelName(TRACE, "TRACE")
|
11
14
|
|
15
|
+
class ExperimentalWarning(Warning):
|
16
|
+
pass
|
12
17
|
|
13
18
|
def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
|
14
19
|
"""Custom TRACE level logging function with redaction."""
|
@@ -16,25 +21,33 @@ def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None
|
|
16
21
|
def _redact_sensitive(data: Any) -> Any:
|
17
22
|
"""Redacts sensitive keys from a dictionary or query string."""
|
18
23
|
REDACT_VALUE = "*" * 8
|
24
|
+
REDACT_KEYS = [
|
25
|
+
"access_token",
|
26
|
+
"authorization",
|
27
|
+
"set-cookie",
|
28
|
+
"cookie",
|
29
|
+
"refresh_token",
|
30
|
+
]
|
19
31
|
if isinstance(data, dict):
|
20
32
|
return {
|
21
|
-
k: (
|
22
|
-
REDACT_VALUE
|
23
|
-
if k.lower() in ["access_token", "authorization", "refresh_token"]
|
24
|
-
else v
|
25
|
-
)
|
33
|
+
k: (REDACT_VALUE if k.lower() in REDACT_KEYS else v)
|
26
34
|
for k, v in data.items()
|
27
35
|
}
|
36
|
+
elif isinstance(data, (list, tuple)):
|
37
|
+
return type(data)(
|
38
|
+
(
|
39
|
+
(item[0], REDACT_VALUE)
|
40
|
+
if isinstance(item, tuple) and item[0].lower() in REDACT_KEYS
|
41
|
+
else item
|
42
|
+
for item in data
|
43
|
+
)
|
44
|
+
)
|
28
45
|
elif isinstance(data, str):
|
29
46
|
parts = data.split("&")
|
30
47
|
for i, part in enumerate(parts):
|
31
48
|
if "=" in part:
|
32
49
|
key, value = part.split("=", 1)
|
33
|
-
if key.lower() in
|
34
|
-
"access_token",
|
35
|
-
"authorization",
|
36
|
-
"refresh_token",
|
37
|
-
]:
|
50
|
+
if key.lower() in REDACT_KEYS:
|
38
51
|
parts[i] = f"{key}={REDACT_VALUE}"
|
39
52
|
return "&".join(parts)
|
40
53
|
return data
|
@@ -70,7 +83,7 @@ class SFAuth:
|
|
70
83
|
access_token: Optional[str] = None,
|
71
84
|
token_expiration_time: Optional[float] = None,
|
72
85
|
token_lifetime: int = 15 * 60,
|
73
|
-
user_agent: str = "sfq/0.0.
|
86
|
+
user_agent: str = "sfq/0.0.11",
|
74
87
|
proxy: str = "auto",
|
75
88
|
) -> None:
|
76
89
|
"""
|
@@ -84,7 +97,7 @@ class SFAuth:
|
|
84
97
|
:param access_token: The access token for the current session (default is None).
|
85
98
|
:param token_expiration_time: The expiration time of the access token (default is None).
|
86
99
|
:param token_lifetime: The lifetime of the access token in seconds (default is 15 minutes).
|
87
|
-
:param user_agent: Custom User-Agent string (default is "sfq/0.0.
|
100
|
+
:param user_agent: Custom User-Agent string (default is "sfq/0.0.11").
|
88
101
|
:param proxy: The proxy configuration, "auto" to use environment (default is "auto").
|
89
102
|
"""
|
90
103
|
self.instance_url = instance_url
|
@@ -139,7 +152,7 @@ class SFAuth:
|
|
139
152
|
logger.trace("Direct connection to %s", netloc)
|
140
153
|
return conn
|
141
154
|
|
142
|
-
def
|
155
|
+
def _new_token_request(self, payload: Dict[str, str]) -> Optional[Dict[str, Any]]:
|
143
156
|
"""
|
144
157
|
Send a POST request to the Salesforce token endpoint using http.client.
|
145
158
|
|
@@ -178,6 +191,7 @@ class SFAuth:
|
|
178
191
|
logger.exception("Error during token request: %s", err)
|
179
192
|
|
180
193
|
finally:
|
194
|
+
logger.trace("Closing connection.")
|
181
195
|
conn.close()
|
182
196
|
|
183
197
|
return None
|
@@ -229,7 +243,7 @@ class SFAuth:
|
|
229
243
|
|
230
244
|
logger.trace("Access token expired or missing, refreshing...")
|
231
245
|
payload = self._prepare_payload()
|
232
|
-
token_data = self.
|
246
|
+
token_data = self._new_token_request(payload)
|
233
247
|
|
234
248
|
if token_data:
|
235
249
|
self.access_token = token_data.get("access_token")
|
@@ -239,7 +253,10 @@ class SFAuth:
|
|
239
253
|
self.org_id = token_data.get("id").split("/")[4]
|
240
254
|
self.user_id = token_data.get("id").split("/")[5]
|
241
255
|
logger.trace(
|
242
|
-
"Authenticated as user %s
|
256
|
+
"Authenticated as user %s for org %s (%s)",
|
257
|
+
self.user_id,
|
258
|
+
self.org_id,
|
259
|
+
token_data.get("instance_url"),
|
243
260
|
)
|
244
261
|
except (IndexError, KeyError):
|
245
262
|
logger.error("Failed to extract org/user IDs from token response.")
|
@@ -264,16 +281,194 @@ class SFAuth:
|
|
264
281
|
logger.warning("Token expiration check failed. Treating token as expired.")
|
265
282
|
return True
|
266
283
|
|
267
|
-
def
|
284
|
+
def read_static_resource_name(
|
285
|
+
self, resource_name: str, namespace: Optional[str] = None
|
286
|
+
) -> Optional[str]:
|
268
287
|
"""
|
269
|
-
|
288
|
+
Read a static resource for a given name from the Salesforce instance.
|
270
289
|
|
271
|
-
:param
|
290
|
+
:param resource_name: Name of the static resource to read.
|
291
|
+
:param namespace: Namespace of the static resource to read (default is None).
|
292
|
+
:return: Static resource content or None on failure.
|
293
|
+
"""
|
294
|
+
_safe_resource_name = quote(resource_name, safe="")
|
295
|
+
query = f"SELECT Id FROM StaticResource WHERE Name = '{_safe_resource_name}'"
|
296
|
+
if namespace:
|
297
|
+
query += f" AND NamespacePrefix = '{namespace}'"
|
298
|
+
query += " LIMIT 1"
|
299
|
+
_static_resource_id_response = self.query(query)
|
300
|
+
|
301
|
+
if (
|
302
|
+
_static_resource_id_response
|
303
|
+
and _static_resource_id_response.get("records")
|
304
|
+
and len(_static_resource_id_response["records"]) > 0
|
305
|
+
):
|
306
|
+
return self.read_static_resource_id(
|
307
|
+
_static_resource_id_response["records"][0].get("Id")
|
308
|
+
)
|
309
|
+
|
310
|
+
logger.error(f"Failed to read static resource with name {_safe_resource_name}.")
|
311
|
+
return None
|
312
|
+
|
313
|
+
def read_static_resource_id(self, resource_id: str) -> Optional[str]:
|
314
|
+
"""
|
315
|
+
Read a static resource for a given ID from the Salesforce instance.
|
316
|
+
|
317
|
+
:param resource_id: ID of the static resource to read.
|
318
|
+
:return: Static resource content or None on failure.
|
319
|
+
"""
|
320
|
+
self._refresh_token_if_needed()
|
321
|
+
|
322
|
+
if not self.access_token:
|
323
|
+
logger.error("No access token available for limits.")
|
324
|
+
return None
|
325
|
+
|
326
|
+
endpoint = f"/services/data/{self.api_version}/sobjects/StaticResource/{resource_id}/Body"
|
327
|
+
headers = {
|
328
|
+
"Authorization": f"Bearer {self.access_token}",
|
329
|
+
"User-Agent": self.user_agent,
|
330
|
+
"Accept": "application/json",
|
331
|
+
}
|
332
|
+
|
333
|
+
parsed_url = urlparse(self.instance_url)
|
334
|
+
conn = self._create_connection(parsed_url.netloc)
|
335
|
+
|
336
|
+
try:
|
337
|
+
logger.trace("Request endpoint: %s", endpoint)
|
338
|
+
logger.trace("Request headers: %s", headers)
|
339
|
+
conn.request("GET", endpoint, headers=headers)
|
340
|
+
response = conn.getresponse()
|
341
|
+
data = response.read().decode("utf-8")
|
342
|
+
self._http_resp_header_logic(response)
|
343
|
+
|
344
|
+
if response.status == 200:
|
345
|
+
logger.debug("Get Static Resource Body API request successful.")
|
346
|
+
logger.trace("Response body: %s", data)
|
347
|
+
return data
|
348
|
+
|
349
|
+
logger.error(
|
350
|
+
"Get Static Resource Body API request failed: %s %s",
|
351
|
+
response.status,
|
352
|
+
response.reason,
|
353
|
+
)
|
354
|
+
logger.debug("Response body: %s", data)
|
355
|
+
|
356
|
+
except Exception as err:
|
357
|
+
logger.exception(
|
358
|
+
"Error during Get Static Resource Body API request: %s", err
|
359
|
+
)
|
360
|
+
|
361
|
+
finally:
|
362
|
+
logger.trace("Closing connection...")
|
363
|
+
conn.close()
|
364
|
+
|
365
|
+
return None
|
366
|
+
|
367
|
+
def update_static_resource_name(
|
368
|
+
self, resource_name: str, data: str, namespace: Optional[str] = None
|
369
|
+
) -> Optional[Dict[str, Any]]:
|
370
|
+
"""
|
371
|
+
Update a static resource for a given name in the Salesforce instance.
|
372
|
+
|
373
|
+
:param resource_name: Name of the static resource to update.
|
374
|
+
:param data: Content to update the static resource with.
|
375
|
+
:param namespace: Optional namespace to search for the static resource.
|
376
|
+
:return: Static resource content or None on failure.
|
377
|
+
"""
|
378
|
+
safe_resource_name = quote(resource_name, safe="")
|
379
|
+
query = f"SELECT Id FROM StaticResource WHERE Name = '{safe_resource_name}'"
|
380
|
+
if namespace:
|
381
|
+
query += f" AND NamespacePrefix = '{namespace}'"
|
382
|
+
query += " LIMIT 1"
|
383
|
+
|
384
|
+
static_resource_id_response = self.query(query)
|
385
|
+
|
386
|
+
if (
|
387
|
+
static_resource_id_response
|
388
|
+
and static_resource_id_response.get("records")
|
389
|
+
and len(static_resource_id_response["records"]) > 0
|
390
|
+
):
|
391
|
+
return self.update_static_resource_id(
|
392
|
+
static_resource_id_response["records"][0].get("Id"), data
|
393
|
+
)
|
394
|
+
|
395
|
+
logger.error(
|
396
|
+
f"Failed to update static resource with name {safe_resource_name}."
|
397
|
+
)
|
398
|
+
return None
|
399
|
+
|
400
|
+
def update_static_resource_id(
|
401
|
+
self, resource_id: str, data: str
|
402
|
+
) -> Optional[Dict[str, Any]]:
|
403
|
+
"""
|
404
|
+
Replace the content of a static resource in the Salesforce instance by ID.
|
405
|
+
|
406
|
+
:param resource_id: ID of the static resource to update.
|
407
|
+
:param data: Content to update the static resource with.
|
272
408
|
:return: Parsed JSON response or None on failure.
|
273
409
|
"""
|
274
|
-
|
410
|
+
self._refresh_token_if_needed()
|
411
|
+
|
412
|
+
if not self.access_token:
|
413
|
+
logger.error("No access token available for limits.")
|
414
|
+
return None
|
415
|
+
|
416
|
+
payload = {"Body": base64.b64encode(data.encode("utf-8"))}
|
417
|
+
|
418
|
+
endpoint = (
|
419
|
+
f"/services/data/{self.api_version}/sobjects/StaticResource/{resource_id}"
|
420
|
+
)
|
421
|
+
headers = {
|
422
|
+
"Authorization": f"Bearer {self.access_token}",
|
423
|
+
"User-Agent": self.user_agent,
|
424
|
+
"Content-Type": "application/json",
|
425
|
+
"Accept": "application/json",
|
426
|
+
}
|
427
|
+
|
428
|
+
parsed_url = urlparse(self.instance_url)
|
429
|
+
conn = self._create_connection(parsed_url.netloc)
|
430
|
+
|
431
|
+
try:
|
432
|
+
logger.trace("Request endpoint: %s", endpoint)
|
433
|
+
logger.trace("Request headers: %s", headers)
|
434
|
+
logger.trace("Request payload: %s", payload)
|
435
|
+
conn.request(
|
436
|
+
"PATCH",
|
437
|
+
endpoint,
|
438
|
+
headers=headers,
|
439
|
+
body=json.dumps(payload, default=lambda x: x.decode("utf-8")),
|
440
|
+
)
|
441
|
+
response = conn.getresponse()
|
442
|
+
data = response.read().decode("utf-8")
|
443
|
+
self._http_resp_header_logic(response)
|
444
|
+
|
445
|
+
if response.status == 200:
|
446
|
+
logger.debug("Patch Static Resource request successful.")
|
447
|
+
logger.trace("Response body: %s", data)
|
448
|
+
return json.loads(data)
|
449
|
+
|
450
|
+
logger.error(
|
451
|
+
"Patch Static Resource API request failed: %s %s",
|
452
|
+
response.status,
|
453
|
+
response.reason,
|
454
|
+
)
|
455
|
+
logger.debug("Response body: %s", data)
|
456
|
+
|
457
|
+
except Exception as err:
|
458
|
+
logger.exception("Error during patch request: %s", err)
|
459
|
+
|
460
|
+
finally:
|
461
|
+
logger.trace("Closing connection.")
|
462
|
+
conn.close()
|
463
|
+
|
464
|
+
return None
|
275
465
|
|
276
466
|
def limits(self) -> Optional[Dict[str, Any]]:
|
467
|
+
"""
|
468
|
+
Execute a GET request to the Salesforce Limits API.
|
469
|
+
|
470
|
+
:return: Parsed JSON response or None on failure.
|
471
|
+
"""
|
277
472
|
self._refresh_token_if_needed()
|
278
473
|
|
279
474
|
if not self.access_token:
|
@@ -284,7 +479,6 @@ class SFAuth:
|
|
284
479
|
headers = {
|
285
480
|
"Authorization": f"Bearer {self.access_token}",
|
286
481
|
"User-Agent": self.user_agent,
|
287
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
288
482
|
"Accept": "application/json",
|
289
483
|
}
|
290
484
|
|
@@ -304,13 +498,16 @@ class SFAuth:
|
|
304
498
|
logger.trace("Response body: %s", data)
|
305
499
|
return json.loads(data)
|
306
500
|
|
307
|
-
logger.error(
|
501
|
+
logger.error(
|
502
|
+
"Limits API request failed: %s %s", response.status, response.reason
|
503
|
+
)
|
308
504
|
logger.debug("Response body: %s", data)
|
309
505
|
|
310
506
|
except Exception as err:
|
311
507
|
logger.exception("Error during limits request: %s", err)
|
312
508
|
|
313
509
|
finally:
|
510
|
+
logger.debug("Closing connection...")
|
314
511
|
conn.close()
|
315
512
|
|
316
513
|
return None
|
@@ -338,7 +535,6 @@ class SFAuth:
|
|
338
535
|
headers = {
|
339
536
|
"Authorization": f"Bearer {self.access_token}",
|
340
537
|
"User-Agent": self.user_agent,
|
341
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
342
538
|
"Accept": "application/json",
|
343
539
|
}
|
344
540
|
|
@@ -393,6 +589,243 @@ class SFAuth:
|
|
393
589
|
logger.exception("Exception during query: %s", err)
|
394
590
|
|
395
591
|
finally:
|
592
|
+
logger.trace("Closing connection...")
|
396
593
|
conn.close()
|
397
594
|
|
398
595
|
return None
|
596
|
+
|
597
|
+
def tooling_query(self, query: str) -> Optional[Dict[str, Any]]:
|
598
|
+
"""
|
599
|
+
Execute a SOQL query using the Tooling API.
|
600
|
+
|
601
|
+
:param query: The SOQL query string.
|
602
|
+
:return: Parsed JSON response or None on failure.
|
603
|
+
"""
|
604
|
+
return self.query(query, tooling=True)
|
605
|
+
|
606
|
+
def _reconnect_with_backoff(self, attempt: int) -> None:
|
607
|
+
wait_time = min(2**attempt, 60)
|
608
|
+
logger.warning(
|
609
|
+
f"Reconnecting after failure, backoff {wait_time}s (attempt {attempt})"
|
610
|
+
)
|
611
|
+
time.sleep(wait_time)
|
612
|
+
self._refresh_token_if_needed()
|
613
|
+
|
614
|
+
def _subscribe_topic(
|
615
|
+
self,
|
616
|
+
topic: str,
|
617
|
+
queue_timeout: int = 90,
|
618
|
+
max_runtime: Optional[int] = None,
|
619
|
+
):
|
620
|
+
"""
|
621
|
+
Yields events from a subscribed Salesforce CometD topic.
|
622
|
+
|
623
|
+
:param topic: Topic to subscribe to, e.g. '/event/MyEvent__e'
|
624
|
+
:param queue_timeout: Seconds to wait for a message before logging heartbeat
|
625
|
+
:param max_runtime: Max total time to listen in seconds (None = unlimited)
|
626
|
+
"""
|
627
|
+
warnings.warn(
|
628
|
+
"The _subscribe_topic method is experimental and subject to change in future versions.",
|
629
|
+
ExperimentalWarning,
|
630
|
+
stacklevel=2,
|
631
|
+
)
|
632
|
+
|
633
|
+
self._refresh_token_if_needed()
|
634
|
+
self._msg_count: int = 0
|
635
|
+
|
636
|
+
if not self.access_token:
|
637
|
+
logger.error("No access token available for event stream.")
|
638
|
+
return
|
639
|
+
|
640
|
+
start_time = time.time()
|
641
|
+
message_queue = Queue()
|
642
|
+
headers = {
|
643
|
+
"Authorization": f"Bearer {self.access_token}",
|
644
|
+
"Content-Type": "application/json",
|
645
|
+
"Accept": "application/json",
|
646
|
+
"User-Agent": self.user_agent,
|
647
|
+
}
|
648
|
+
|
649
|
+
parsed_url = urlparse(self.instance_url)
|
650
|
+
conn = self._create_connection(parsed_url.netloc)
|
651
|
+
_API_VERSION = str(self.api_version).removeprefix("v")
|
652
|
+
client_id = str()
|
653
|
+
|
654
|
+
try:
|
655
|
+
logger.trace("Starting handshake with Salesforce CometD server.")
|
656
|
+
handshake_payload = json.dumps(
|
657
|
+
{
|
658
|
+
"id": str(self._msg_count + 1),
|
659
|
+
"version": "1.0",
|
660
|
+
"minimumVersion": "1.0",
|
661
|
+
"channel": "/meta/handshake",
|
662
|
+
"supportedConnectionTypes": ["long-polling"],
|
663
|
+
"advice": {"timeout": 60000, "interval": 0},
|
664
|
+
}
|
665
|
+
)
|
666
|
+
conn.request(
|
667
|
+
"POST",
|
668
|
+
f"/cometd/{_API_VERSION}/meta/handshake",
|
669
|
+
headers=headers,
|
670
|
+
body=handshake_payload,
|
671
|
+
)
|
672
|
+
response = conn.getresponse()
|
673
|
+
self._http_resp_header_logic(response)
|
674
|
+
|
675
|
+
logger.trace("Received handshake response.")
|
676
|
+
for name, value in response.getheaders():
|
677
|
+
if name.lower() == "set-cookie" and "BAYEUX_BROWSER=" in value:
|
678
|
+
_bayeux_browser_cookie = value.split("BAYEUX_BROWSER=")[1].split(
|
679
|
+
";"
|
680
|
+
)[0]
|
681
|
+
headers["Cookie"] = f"BAYEUX_BROWSER={_bayeux_browser_cookie}"
|
682
|
+
break
|
683
|
+
|
684
|
+
data = json.loads(response.read().decode("utf-8"))
|
685
|
+
if not data or not data[0].get("successful"):
|
686
|
+
logger.error("Handshake failed: %s", data)
|
687
|
+
return
|
688
|
+
|
689
|
+
client_id = data[0]["clientId"]
|
690
|
+
logger.trace(f"Handshake successful, client ID: {client_id}")
|
691
|
+
|
692
|
+
logger.trace(f"Subscribing to topic: {topic}")
|
693
|
+
subscribe_message = {
|
694
|
+
"channel": "/meta/subscribe",
|
695
|
+
"clientId": client_id,
|
696
|
+
"subscription": topic,
|
697
|
+
"id": str(self._msg_count + 1),
|
698
|
+
}
|
699
|
+
conn.request(
|
700
|
+
"POST",
|
701
|
+
f"/cometd/{_API_VERSION}/meta/subscribe",
|
702
|
+
headers=headers,
|
703
|
+
body=json.dumps(subscribe_message),
|
704
|
+
)
|
705
|
+
response = conn.getresponse()
|
706
|
+
self._http_resp_header_logic(response)
|
707
|
+
|
708
|
+
sub_response = json.loads(response.read().decode("utf-8"))
|
709
|
+
if not sub_response or not sub_response[0].get("successful"):
|
710
|
+
logger.error("Subscription failed: %s", sub_response)
|
711
|
+
return
|
712
|
+
|
713
|
+
logger.info(f"Successfully subscribed to topic: {topic}")
|
714
|
+
logger.trace("Entering event polling loop.")
|
715
|
+
|
716
|
+
try:
|
717
|
+
while True:
|
718
|
+
if max_runtime and (time.time() - start_time > max_runtime):
|
719
|
+
logger.info(
|
720
|
+
f"Disconnecting after max_runtime={max_runtime} seconds"
|
721
|
+
)
|
722
|
+
break
|
723
|
+
|
724
|
+
logger.trace("Sending connection message.")
|
725
|
+
connect_payload = json.dumps(
|
726
|
+
[
|
727
|
+
{
|
728
|
+
"channel": "/meta/connect",
|
729
|
+
"clientId": client_id,
|
730
|
+
"connectionType": "long-polling",
|
731
|
+
"id": str(self._msg_count + 1),
|
732
|
+
}
|
733
|
+
]
|
734
|
+
)
|
735
|
+
|
736
|
+
max_retries = 5
|
737
|
+
attempt = 0
|
738
|
+
|
739
|
+
while attempt < max_retries:
|
740
|
+
try:
|
741
|
+
conn.request(
|
742
|
+
"POST",
|
743
|
+
f"/cometd/{_API_VERSION}/meta/connect",
|
744
|
+
headers=headers,
|
745
|
+
body=connect_payload,
|
746
|
+
)
|
747
|
+
response = conn.getresponse()
|
748
|
+
self._http_resp_header_logic(response)
|
749
|
+
self._msg_count += 1
|
750
|
+
|
751
|
+
events = json.loads(response.read().decode("utf-8"))
|
752
|
+
for event in events:
|
753
|
+
if event.get("channel") == topic and "data" in event:
|
754
|
+
logger.trace(
|
755
|
+
f"Event received for topic {topic}, data: {event['data']}"
|
756
|
+
)
|
757
|
+
message_queue.put(event)
|
758
|
+
break
|
759
|
+
except (
|
760
|
+
http.client.RemoteDisconnected,
|
761
|
+
ConnectionResetError,
|
762
|
+
TimeoutError,
|
763
|
+
http.client.BadStatusLine,
|
764
|
+
http.client.CannotSendRequest,
|
765
|
+
ConnectionAbortedError,
|
766
|
+
ConnectionRefusedError,
|
767
|
+
ConnectionError,
|
768
|
+
) as e:
|
769
|
+
logger.warning(
|
770
|
+
f"Connection error (attempt {attempt + 1}): {e}"
|
771
|
+
)
|
772
|
+
conn.close()
|
773
|
+
conn = self._create_connection(parsed_url.netloc)
|
774
|
+
self._reconnect_with_backoff(attempt)
|
775
|
+
attempt += 1
|
776
|
+
except Exception as e:
|
777
|
+
logger.exception(
|
778
|
+
f"Connection error (attempt {attempt + 1}): {e}"
|
779
|
+
)
|
780
|
+
break
|
781
|
+
else:
|
782
|
+
logger.error("Max retries reached. Exiting event stream.")
|
783
|
+
break
|
784
|
+
|
785
|
+
while True:
|
786
|
+
try:
|
787
|
+
msg = message_queue.get(timeout=queue_timeout, block=True)
|
788
|
+
yield msg
|
789
|
+
except Empty:
|
790
|
+
logger.debug(
|
791
|
+
f"Heartbeat: no message in last {queue_timeout} seconds"
|
792
|
+
)
|
793
|
+
break
|
794
|
+
except KeyboardInterrupt:
|
795
|
+
logger.info("Received keyboard interrupt, disconnecting...")
|
796
|
+
|
797
|
+
except Exception as e:
|
798
|
+
logger.exception(f"Polling error: {e}")
|
799
|
+
|
800
|
+
finally:
|
801
|
+
if client_id:
|
802
|
+
try:
|
803
|
+
logger.trace(
|
804
|
+
f"Disconnecting from server with client ID: {client_id}"
|
805
|
+
)
|
806
|
+
disconnect_payload = json.dumps(
|
807
|
+
[
|
808
|
+
{
|
809
|
+
"channel": "/meta/disconnect",
|
810
|
+
"clientId": client_id,
|
811
|
+
"id": str(self._msg_count + 1),
|
812
|
+
}
|
813
|
+
]
|
814
|
+
)
|
815
|
+
conn.request(
|
816
|
+
"POST",
|
817
|
+
f"/cometd/{_API_VERSION}/meta/disconnect",
|
818
|
+
headers=headers,
|
819
|
+
body=disconnect_payload,
|
820
|
+
)
|
821
|
+
response = conn.getresponse()
|
822
|
+
self._http_resp_header_logic(response)
|
823
|
+
_ = response.read()
|
824
|
+
logger.trace("Disconnected successfully.")
|
825
|
+
except Exception as e:
|
826
|
+
logger.warning(f"Exception during disconnect: {e}")
|
827
|
+
if conn:
|
828
|
+
logger.trace("Closing connection.")
|
829
|
+
conn.close()
|
830
|
+
|
831
|
+
logger.trace("Leaving event polling loop.")
|
@@ -0,0 +1,5 @@
|
|
1
|
+
sfq/__init__.py,sha256=QkMVcIOaQO7XP29zK9l7uQCTx0NcqIDBNj8V08Llu24,31595
|
2
|
+
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
sfq-0.0.11.dist-info/METADATA,sha256=F1sqI833stEXIk9Z7jtG6q0F8dPuEGMDW8xgFgGxa4M,5067
|
4
|
+
sfq-0.0.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
sfq-0.0.11.dist-info/RECORD,,
|
sfq-0.0.9.dist-info/RECORD
DELETED
@@ -1,5 +0,0 @@
|
|
1
|
-
sfq/__init__.py,sha256=TUhdE13kmM814DmbYgDHtQogRm461k17YKr7YKzBNho,15101
|
2
|
-
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
sfq-0.0.9.dist-info/METADATA,sha256=DCsSMcv_Nzq2JJinOYzab8Q7MU0jjFQ27lGgMZjUnEI,5066
|
4
|
-
sfq-0.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
sfq-0.0.9.dist-info/RECORD,,
|
File without changes
|