sfq 0.0.32__py3-none-any.whl → 0.0.33__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 +223 -896
- sfq/_cometd.py +7 -10
- sfq/auth.py +401 -0
- sfq/crud.py +446 -0
- sfq/exceptions.py +54 -0
- sfq/http_client.py +319 -0
- sfq/query.py +398 -0
- sfq/soap.py +181 -0
- sfq/utils.py +196 -0
- {sfq-0.0.32.dist-info → sfq-0.0.33.dist-info}/METADATA +1 -1
- sfq-0.0.33.dist-info/RECORD +13 -0
- sfq-0.0.32.dist-info/RECORD +0 -6
- {sfq-0.0.32.dist-info → sfq-0.0.33.dist-info}/WHEEL +0 -0
sfq/utils.py
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions and logging configuration for the SFQ library.
|
3
|
+
|
4
|
+
This module contains shared utilities, logging configuration, and helper functions
|
5
|
+
used throughout the SFQ library, including the custom TRACE logging level and
|
6
|
+
sensitive data redaction functionality.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import json
|
10
|
+
import logging
|
11
|
+
import re
|
12
|
+
from typing import Any, Dict, List, Tuple, Union
|
13
|
+
|
14
|
+
# Custom TRACE logging level
|
15
|
+
TRACE = 5
|
16
|
+
logging.addLevelName(TRACE, "TRACE")
|
17
|
+
|
18
|
+
|
19
|
+
def _redact_sensitive(data: Any) -> Any:
|
20
|
+
"""
|
21
|
+
Redacts sensitive keys from a dictionary, query string, or sessionId.
|
22
|
+
|
23
|
+
This function recursively processes data structures to remove or mask
|
24
|
+
sensitive information like tokens, passwords, and session IDs.
|
25
|
+
|
26
|
+
:param data: The data to redact (dict, list, tuple, or string)
|
27
|
+
:return: The data with sensitive information redacted
|
28
|
+
"""
|
29
|
+
REDACT_VALUE = "*" * 8
|
30
|
+
REDACT_KEYS = [
|
31
|
+
"access_token",
|
32
|
+
"authorization",
|
33
|
+
"set-cookie",
|
34
|
+
"cookie",
|
35
|
+
"refresh_token",
|
36
|
+
"client_secret",
|
37
|
+
"sessionid",
|
38
|
+
]
|
39
|
+
|
40
|
+
if isinstance(data, dict):
|
41
|
+
return {
|
42
|
+
k: (REDACT_VALUE if k.lower() in REDACT_KEYS else v)
|
43
|
+
for k, v in data.items()
|
44
|
+
}
|
45
|
+
elif isinstance(data, (list, tuple)):
|
46
|
+
return type(data)(
|
47
|
+
(
|
48
|
+
(item[0], REDACT_VALUE)
|
49
|
+
if isinstance(item, tuple) and item[0].lower() in REDACT_KEYS
|
50
|
+
else item
|
51
|
+
for item in data
|
52
|
+
)
|
53
|
+
)
|
54
|
+
elif isinstance(data, str):
|
55
|
+
# Redact sessionId in XML
|
56
|
+
if "<sessionId>" in data and "</sessionId>" in data:
|
57
|
+
data = re.sub(
|
58
|
+
r"(<sessionId>)(.*?)(</sessionId>)",
|
59
|
+
r"\1{}\3".format(REDACT_VALUE),
|
60
|
+
data,
|
61
|
+
)
|
62
|
+
# Redact query string parameters
|
63
|
+
parts = data.split("&")
|
64
|
+
for i, part in enumerate(parts):
|
65
|
+
if "=" in part:
|
66
|
+
key, value = part.split("=", 1)
|
67
|
+
if key.lower() in REDACT_KEYS:
|
68
|
+
parts[i] = f"{key}={REDACT_VALUE}"
|
69
|
+
return "&".join(parts)
|
70
|
+
|
71
|
+
return data
|
72
|
+
|
73
|
+
|
74
|
+
def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
|
75
|
+
"""
|
76
|
+
Custom TRACE level logging function with redaction.
|
77
|
+
|
78
|
+
This function adds a custom TRACE logging level that automatically
|
79
|
+
redacts sensitive information from log messages.
|
80
|
+
|
81
|
+
:param self: The logger instance
|
82
|
+
:param message: The log message
|
83
|
+
:param args: Additional arguments for the log message
|
84
|
+
:param kwargs: Additional keyword arguments for logging
|
85
|
+
"""
|
86
|
+
redacted_args = args
|
87
|
+
if args:
|
88
|
+
first = args[0]
|
89
|
+
if isinstance(first, str):
|
90
|
+
try:
|
91
|
+
loaded = json.loads(first)
|
92
|
+
first = loaded
|
93
|
+
except (json.JSONDecodeError, TypeError):
|
94
|
+
pass
|
95
|
+
redacted_first = _redact_sensitive(first)
|
96
|
+
redacted_args = (redacted_first,) + args[1:]
|
97
|
+
|
98
|
+
if self.isEnabledFor(TRACE):
|
99
|
+
self._log(TRACE, message, redacted_args, **kwargs)
|
100
|
+
|
101
|
+
|
102
|
+
# Add the trace method to the Logger class
|
103
|
+
logging.Logger.trace = trace
|
104
|
+
|
105
|
+
|
106
|
+
def get_logger(name: str = "sfq") -> logging.Logger:
|
107
|
+
"""
|
108
|
+
Get a logger instance with the custom TRACE level configured.
|
109
|
+
|
110
|
+
:param name: The logger name (defaults to "sfq")
|
111
|
+
:return: Configured logger instance
|
112
|
+
"""
|
113
|
+
return logging.getLogger(name)
|
114
|
+
|
115
|
+
|
116
|
+
def format_headers_for_logging(
|
117
|
+
headers: Union[Dict[str, str], List[Tuple[str, str]]],
|
118
|
+
) -> List[Tuple[str, str]]:
|
119
|
+
"""
|
120
|
+
Format headers for logging, filtering out sensitive browser information.
|
121
|
+
|
122
|
+
:param headers: Headers as dict or list of tuples
|
123
|
+
:return: Filtered list of header tuples suitable for logging
|
124
|
+
"""
|
125
|
+
if isinstance(headers, dict):
|
126
|
+
headers_list = list(headers.items())
|
127
|
+
else:
|
128
|
+
headers_list = list(headers)
|
129
|
+
|
130
|
+
# Filter out BrowserId cookies and other sensitive headers
|
131
|
+
return [(k, v) for k, v in headers_list if not v.startswith("BrowserId=")]
|
132
|
+
|
133
|
+
|
134
|
+
def parse_api_usage_from_header(sforce_limit_info: str) -> Tuple[int, int, float]:
|
135
|
+
"""
|
136
|
+
Parse API usage information from Sforce-Limit-Info header.
|
137
|
+
|
138
|
+
:param sforce_limit_info: The Sforce-Limit-Info header value
|
139
|
+
:return: Tuple of (current_calls, max_calls, usage_percentage)
|
140
|
+
"""
|
141
|
+
try:
|
142
|
+
# Expected format: "api-usage=123/15000"
|
143
|
+
usage_part = sforce_limit_info.split("=")[1]
|
144
|
+
current_api_calls = int(usage_part.split("/")[0])
|
145
|
+
maximum_api_calls = int(usage_part.split("/")[1])
|
146
|
+
usage_percentage = round(current_api_calls / maximum_api_calls * 100, 2)
|
147
|
+
return current_api_calls, maximum_api_calls, usage_percentage
|
148
|
+
except (IndexError, ValueError, ZeroDivisionError) as e:
|
149
|
+
logger = get_logger()
|
150
|
+
logger.warning("Failed to parse API usage from header: %s", e)
|
151
|
+
return 0, 0, 0.0
|
152
|
+
|
153
|
+
|
154
|
+
def log_api_usage(sforce_limit_info: str, high_usage_threshold: int = 80) -> None:
|
155
|
+
"""
|
156
|
+
Log API usage information with appropriate warning levels.
|
157
|
+
|
158
|
+
:param sforce_limit_info: The Sforce-Limit-Info header value
|
159
|
+
:param high_usage_threshold: Threshold percentage for high usage warning
|
160
|
+
"""
|
161
|
+
logger = get_logger()
|
162
|
+
current_calls, max_calls, usage_percentage = parse_api_usage_from_header(
|
163
|
+
sforce_limit_info
|
164
|
+
)
|
165
|
+
|
166
|
+
if usage_percentage > high_usage_threshold:
|
167
|
+
logger.warning(
|
168
|
+
"High API usage: %s/%s (%s%%)",
|
169
|
+
current_calls,
|
170
|
+
max_calls,
|
171
|
+
usage_percentage,
|
172
|
+
)
|
173
|
+
else:
|
174
|
+
logger.debug(
|
175
|
+
"API usage: %s/%s (%s%%)",
|
176
|
+
current_calls,
|
177
|
+
max_calls,
|
178
|
+
usage_percentage,
|
179
|
+
)
|
180
|
+
|
181
|
+
|
182
|
+
def extract_org_and_user_ids(token_id_url: str) -> Tuple[str, str]:
|
183
|
+
"""
|
184
|
+
Extract organization and user IDs from the token response ID URL.
|
185
|
+
|
186
|
+
:param token_id_url: The ID URL from the token response
|
187
|
+
:return: Tuple of (org_id, user_id)
|
188
|
+
:raises ValueError: If the URL format is invalid
|
189
|
+
"""
|
190
|
+
try:
|
191
|
+
parts = token_id_url.split("/")
|
192
|
+
org_id = parts[4]
|
193
|
+
user_id = parts[5]
|
194
|
+
return org_id, user_id
|
195
|
+
except (IndexError, AttributeError):
|
196
|
+
raise ValueError(f"Invalid token ID URL format: {token_id_url}")
|
@@ -0,0 +1,13 @@
|
|
1
|
+
sfq/__init__.py,sha256=kTT0wRF7zWpG-2KYwvPwDlruzx0SB9fqIxGXq4P51oE,20134
|
2
|
+
sfq/_cometd.py,sha256=QqdSGsms9uFm7vgmxgau7m2UuLHztK1yjN-BNjeo8xM,10381
|
3
|
+
sfq/auth.py,sha256=bD7kEI5UpUAh0xpE2GzB7EatfLE0q-rqG7tOpqn_cQY,13985
|
4
|
+
sfq/crud.py,sha256=oYx74HJN18fnsVVfEUIGb-H9YN0EAgH3HmjktN4GjuM,17829
|
5
|
+
sfq/exceptions.py,sha256=HZctvGj1SGguca0oG6fqSmf3KDbq4v68FfQfqB-crpo,906
|
6
|
+
sfq/http_client.py,sha256=JuPZ4YYR5y0Hhcn1HffMMcQcJPS_rht-1RewrTWn7bg,11636
|
7
|
+
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
sfq/query.py,sha256=AoagL8PMKUcpbPPTcHJPKhmUdDDPa0La4JLC0TUN_Yc,14586
|
9
|
+
sfq/soap.py,sha256=jQkC6D4z5iHFkDFOQhaR9Saj4sy3Qxn_zp0VPD-BmlQ,6728
|
10
|
+
sfq/utils.py,sha256=gx_pCZmOykYz19wwx6O2BTGj7bQfzhX_SuLHnYGCWuc,6234
|
11
|
+
sfq-0.0.33.dist-info/METADATA,sha256=qOEjgwIpgmx4J-ca_1knpMPVZU_zoBTqcO7ic_nWwIw,6899
|
12
|
+
sfq-0.0.33.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
13
|
+
sfq-0.0.33.dist-info/RECORD,,
|
sfq-0.0.32.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
sfq/__init__.py,sha256=sGL_DT0w4-9GqmTD_rffZ_J3eqOtjFUuH1S4nlVzow8,47102
|
2
|
-
sfq/_cometd.py,sha256=XimQEubmJwUmbWe85TxH_cuhGvWVuiHHrVr41tguuiI,10508
|
3
|
-
sfq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
sfq-0.0.32.dist-info/METADATA,sha256=Y3avdu7TKc8BCEz2kHZaqjPAIqcjD4CHzSF_WdMO4pI,6899
|
5
|
-
sfq-0.0.32.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
6
|
-
sfq-0.0.32.dist-info/RECORD,,
|
File without changes
|