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/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}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfq
3
- Version: 0.0.32
3
+ Version: 0.0.33
4
4
  Summary: Python wrapper for the Salesforce's Query API.
5
5
  Author-email: David Moruzzi <sfq.pypi@dmoruzi.com>
6
6
  Keywords: salesforce,salesforce query
@@ -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,,
@@ -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