aio-sf 0.1.0b1__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.
- aio_salesforce/__init__.py +27 -0
- aio_salesforce/api/README.md +107 -0
- aio_salesforce/api/__init__.py +65 -0
- aio_salesforce/api/bulk_v2/__init__.py +21 -0
- aio_salesforce/api/bulk_v2/client.py +200 -0
- aio_salesforce/api/bulk_v2/types.py +71 -0
- aio_salesforce/api/describe/__init__.py +31 -0
- aio_salesforce/api/describe/client.py +94 -0
- aio_salesforce/api/describe/types.py +303 -0
- aio_salesforce/api/query/__init__.py +18 -0
- aio_salesforce/api/query/client.py +216 -0
- aio_salesforce/api/query/types.py +38 -0
- aio_salesforce/api/types.py +303 -0
- aio_salesforce/connection.py +511 -0
- aio_salesforce/exporter/__init__.py +38 -0
- aio_salesforce/exporter/bulk_export.py +397 -0
- aio_salesforce/exporter/parquet_writer.py +296 -0
- aio_salesforce/exporter/parquet_writer.py.backup +326 -0
- aio_sf-0.1.0b1.dist-info/METADATA +198 -0
- aio_sf-0.1.0b1.dist-info/RECORD +22 -0
- aio_sf-0.1.0b1.dist-info/WHEEL +4 -0
- aio_sf-0.1.0b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypedDict definitions for Salesforce API responses.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, TypedDict, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SalesforceAttributes(TypedDict):
|
|
9
|
+
"""Standard Salesforce record attributes."""
|
|
10
|
+
|
|
11
|
+
type: str
|
|
12
|
+
url: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OrganizationInfo(TypedDict):
|
|
16
|
+
"""Organization information from SOQL query."""
|
|
17
|
+
|
|
18
|
+
attributes: SalesforceAttributes
|
|
19
|
+
Id: str
|
|
20
|
+
Name: str
|
|
21
|
+
OrganizationType: str
|
|
22
|
+
InstanceName: str
|
|
23
|
+
IsSandbox: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LimitInfo(TypedDict):
|
|
27
|
+
"""Individual limit information."""
|
|
28
|
+
|
|
29
|
+
Max: int
|
|
30
|
+
Remaining: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OrganizationLimits(TypedDict, total=False):
|
|
34
|
+
"""Organization limits - using total=False since keys vary by org."""
|
|
35
|
+
|
|
36
|
+
# API Limits
|
|
37
|
+
DailyApiRequests: LimitInfo
|
|
38
|
+
DailyBulkApiBatches: LimitInfo
|
|
39
|
+
DailyBulkV2QueryJobs: LimitInfo
|
|
40
|
+
DailyStreamingApiEvents: LimitInfo
|
|
41
|
+
|
|
42
|
+
# Storage Limits
|
|
43
|
+
DataStorageMB: LimitInfo
|
|
44
|
+
FileStorageMB: LimitInfo
|
|
45
|
+
|
|
46
|
+
# Email Limits
|
|
47
|
+
DailyWorkflowEmails: LimitInfo
|
|
48
|
+
MassEmail: LimitInfo
|
|
49
|
+
SingleEmail: LimitInfo
|
|
50
|
+
|
|
51
|
+
# Other common limits - there are many more, but these are the most common
|
|
52
|
+
# Using Any for the rest since there are 60+ different limit types
|
|
53
|
+
# and they vary by org type and features
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SObjectUrls(TypedDict):
|
|
57
|
+
"""URLs for various SObject operations."""
|
|
58
|
+
|
|
59
|
+
sobject: str
|
|
60
|
+
describe: str
|
|
61
|
+
rowTemplate: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SObjectInfo(TypedDict):
|
|
65
|
+
"""Basic SObject information from list_sobjects."""
|
|
66
|
+
|
|
67
|
+
activateable: bool
|
|
68
|
+
associateEntityType: Optional[str]
|
|
69
|
+
associateParentEntity: Optional[str]
|
|
70
|
+
createable: bool
|
|
71
|
+
custom: bool
|
|
72
|
+
customSetting: bool
|
|
73
|
+
deepCloneable: bool
|
|
74
|
+
deletable: bool
|
|
75
|
+
deprecatedAndHidden: bool
|
|
76
|
+
feedEnabled: bool
|
|
77
|
+
hasSubtypes: bool
|
|
78
|
+
isInterface: bool
|
|
79
|
+
isSubtype: bool
|
|
80
|
+
keyPrefix: Optional[str]
|
|
81
|
+
label: str
|
|
82
|
+
labelPlural: str
|
|
83
|
+
layoutable: bool
|
|
84
|
+
mergeable: bool
|
|
85
|
+
mruEnabled: bool
|
|
86
|
+
name: str
|
|
87
|
+
queryable: bool
|
|
88
|
+
replicateable: bool
|
|
89
|
+
retrieveable: bool
|
|
90
|
+
searchable: bool
|
|
91
|
+
triggerable: bool
|
|
92
|
+
undeletable: bool
|
|
93
|
+
updateable: bool
|
|
94
|
+
urls: SObjectUrls
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ActionOverride(TypedDict):
|
|
98
|
+
"""Action override information."""
|
|
99
|
+
|
|
100
|
+
formFactor: str
|
|
101
|
+
isAvailableInTouch: bool
|
|
102
|
+
name: str
|
|
103
|
+
pageId: Optional[str]
|
|
104
|
+
url: Optional[str]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ChildRelationship(TypedDict):
|
|
108
|
+
"""Child relationship information."""
|
|
109
|
+
|
|
110
|
+
cascadeDelete: bool
|
|
111
|
+
childSObject: str
|
|
112
|
+
deprecatedAndHidden: bool
|
|
113
|
+
field: str
|
|
114
|
+
junctionIdListNames: List[str]
|
|
115
|
+
junctionReferenceTo: List[str]
|
|
116
|
+
relationshipName: Optional[str]
|
|
117
|
+
restrictedDelete: bool
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PicklistValue(TypedDict):
|
|
121
|
+
"""Picklist value information."""
|
|
122
|
+
|
|
123
|
+
active: bool
|
|
124
|
+
defaultValue: bool
|
|
125
|
+
label: str
|
|
126
|
+
validFor: Optional[str]
|
|
127
|
+
value: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FilteredLookupInfo(TypedDict):
|
|
131
|
+
"""Filtered lookup information."""
|
|
132
|
+
|
|
133
|
+
controllingFields: List[str]
|
|
134
|
+
dependent: bool
|
|
135
|
+
optionalFilter: bool
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class RecordTypeInfo(TypedDict):
|
|
139
|
+
"""Record type information."""
|
|
140
|
+
|
|
141
|
+
available: bool
|
|
142
|
+
defaultRecordTypeMapping: bool
|
|
143
|
+
developerName: str
|
|
144
|
+
master: bool
|
|
145
|
+
name: str
|
|
146
|
+
recordTypeId: str
|
|
147
|
+
urls: Dict[str, str]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class NamedLayoutInfo(TypedDict):
|
|
151
|
+
"""Named layout information."""
|
|
152
|
+
|
|
153
|
+
name: str
|
|
154
|
+
urls: Dict[str, str]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ScopeInfo(TypedDict):
|
|
158
|
+
"""Scope information."""
|
|
159
|
+
|
|
160
|
+
label: str
|
|
161
|
+
name: str
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class FieldInfo(TypedDict, total=False):
|
|
165
|
+
"""Field information from describe_sobject - using total=False due to many optional fields."""
|
|
166
|
+
|
|
167
|
+
# Core field properties (always present)
|
|
168
|
+
name: str
|
|
169
|
+
type: str
|
|
170
|
+
label: str
|
|
171
|
+
|
|
172
|
+
# Common properties (usually present)
|
|
173
|
+
length: int
|
|
174
|
+
nillable: bool
|
|
175
|
+
createable: bool
|
|
176
|
+
updateable: bool
|
|
177
|
+
custom: bool
|
|
178
|
+
|
|
179
|
+
# Boolean flags
|
|
180
|
+
aggregatable: bool
|
|
181
|
+
aiPredictionField: bool
|
|
182
|
+
autoNumber: bool
|
|
183
|
+
calculated: bool
|
|
184
|
+
caseSensitive: bool
|
|
185
|
+
dependentPicklist: bool
|
|
186
|
+
deprecatedAndHidden: bool
|
|
187
|
+
encrypted: bool
|
|
188
|
+
externalId: bool
|
|
189
|
+
filterable: bool
|
|
190
|
+
formulaTreatNullNumberAsZero: bool
|
|
191
|
+
groupable: bool
|
|
192
|
+
highScaleNumber: bool
|
|
193
|
+
htmlFormatted: bool
|
|
194
|
+
idLookup: bool
|
|
195
|
+
nameField: bool
|
|
196
|
+
namePointing: bool
|
|
197
|
+
permissionable: bool
|
|
198
|
+
polymorphicForeignKey: bool
|
|
199
|
+
queryByDistance: bool
|
|
200
|
+
restrictedPicklist: bool
|
|
201
|
+
searchPrefilterable: bool
|
|
202
|
+
sortable: bool
|
|
203
|
+
unique: bool
|
|
204
|
+
writeRequiresMasterRead: bool
|
|
205
|
+
|
|
206
|
+
# Numeric properties
|
|
207
|
+
byteLength: int
|
|
208
|
+
digits: int
|
|
209
|
+
precision: int
|
|
210
|
+
scale: int
|
|
211
|
+
|
|
212
|
+
# String properties
|
|
213
|
+
calculatedFormula: Optional[str]
|
|
214
|
+
compoundFieldName: Optional[str]
|
|
215
|
+
controllerName: Optional[str]
|
|
216
|
+
defaultValue: Optional[str]
|
|
217
|
+
defaultValueFormula: Optional[str]
|
|
218
|
+
extraTypeInfo: Optional[str]
|
|
219
|
+
inlineHelpText: Optional[str]
|
|
220
|
+
mask: Optional[str]
|
|
221
|
+
maskType: Optional[str]
|
|
222
|
+
referenceTargetField: Optional[str]
|
|
223
|
+
relationshipName: Optional[str]
|
|
224
|
+
soapType: str
|
|
225
|
+
|
|
226
|
+
# Boolean properties with defaults
|
|
227
|
+
cascadeDelete: bool
|
|
228
|
+
defaultedOnCreate: bool
|
|
229
|
+
displayLocationInDecimal: bool
|
|
230
|
+
restrictedDelete: bool
|
|
231
|
+
|
|
232
|
+
# Numeric properties
|
|
233
|
+
relationshipOrder: Optional[int]
|
|
234
|
+
|
|
235
|
+
# Array properties
|
|
236
|
+
picklistValues: List[PicklistValue]
|
|
237
|
+
referenceTo: List[str]
|
|
238
|
+
|
|
239
|
+
# Object properties
|
|
240
|
+
filteredLookupInfo: Optional[FilteredLookupInfo]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class SObjectDescribe(TypedDict):
|
|
244
|
+
"""Complete SObject describe information."""
|
|
245
|
+
|
|
246
|
+
# Basic properties
|
|
247
|
+
name: str
|
|
248
|
+
label: str
|
|
249
|
+
labelPlural: str
|
|
250
|
+
keyPrefix: Optional[str]
|
|
251
|
+
custom: bool
|
|
252
|
+
|
|
253
|
+
# Capabilities
|
|
254
|
+
activateable: bool
|
|
255
|
+
createable: bool
|
|
256
|
+
deletable: bool
|
|
257
|
+
mergeable: bool
|
|
258
|
+
queryable: bool
|
|
259
|
+
replicateable: bool
|
|
260
|
+
retrieveable: bool
|
|
261
|
+
searchable: bool
|
|
262
|
+
triggerable: bool
|
|
263
|
+
undeletable: bool
|
|
264
|
+
updateable: bool
|
|
265
|
+
|
|
266
|
+
# Layout and UI properties
|
|
267
|
+
compactLayoutable: bool
|
|
268
|
+
feedEnabled: bool
|
|
269
|
+
layoutable: bool
|
|
270
|
+
listviewable: bool
|
|
271
|
+
lookupLayoutable: bool
|
|
272
|
+
mruEnabled: bool
|
|
273
|
+
searchLayoutable: bool
|
|
274
|
+
|
|
275
|
+
# Advanced properties
|
|
276
|
+
customSetting: bool
|
|
277
|
+
deepCloneable: bool
|
|
278
|
+
deprecatedAndHidden: bool
|
|
279
|
+
hasSubtypes: bool
|
|
280
|
+
isInterface: bool
|
|
281
|
+
isSubtype: bool
|
|
282
|
+
|
|
283
|
+
# Optional properties
|
|
284
|
+
associateEntityType: Optional[str]
|
|
285
|
+
associateParentEntity: Optional[str]
|
|
286
|
+
defaultImplementation: Optional[str]
|
|
287
|
+
extendedBy: Optional[str]
|
|
288
|
+
extendsInterfaces: Optional[str]
|
|
289
|
+
implementedBy: Optional[str]
|
|
290
|
+
implementsInterfaces: Optional[str]
|
|
291
|
+
networkScopeFieldName: Optional[str]
|
|
292
|
+
sobjectDescribeOption: Optional[str]
|
|
293
|
+
|
|
294
|
+
# Array properties
|
|
295
|
+
actionOverrides: List[ActionOverride]
|
|
296
|
+
childRelationships: List[ChildRelationship]
|
|
297
|
+
fields: List[FieldInfo]
|
|
298
|
+
namedLayoutInfos: List[NamedLayoutInfo]
|
|
299
|
+
recordTypeInfos: List[RecordTypeInfo]
|
|
300
|
+
supportedScopes: List[ScopeInfo]
|
|
301
|
+
|
|
302
|
+
# URLs
|
|
303
|
+
urls: Dict[str, str]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Salesforce Query API module."""
|
|
2
|
+
|
|
3
|
+
from .client import QueryAPI, QueryResult
|
|
4
|
+
from .types import (
|
|
5
|
+
QueryResponse,
|
|
6
|
+
QueryAllResponse,
|
|
7
|
+
QueryMoreResponse,
|
|
8
|
+
QueryErrorResponse,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"QueryAPI",
|
|
13
|
+
"QueryResult",
|
|
14
|
+
"QueryResponse",
|
|
15
|
+
"QueryAllResponse",
|
|
16
|
+
"QueryMoreResponse",
|
|
17
|
+
"QueryErrorResponse",
|
|
18
|
+
]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Salesforce Query API client."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from typing import Any, Dict, List, Optional, AsyncGenerator
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ...connection import SalesforceConnection
|
|
12
|
+
|
|
13
|
+
from .types import QueryResponse, QueryMoreResponse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QueryResult:
|
|
17
|
+
"""
|
|
18
|
+
A query result that supports len() and acts as an async iterator over individual records.
|
|
19
|
+
Handles pagination automatically via QueryMore.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
query_api: "QueryAPI",
|
|
25
|
+
initial_response: QueryResponse,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Initialize QueryResult.
|
|
29
|
+
|
|
30
|
+
:param query_api: QueryAPI instance for making follow-up requests
|
|
31
|
+
:param initial_response: Initial query response
|
|
32
|
+
"""
|
|
33
|
+
self._query_api = query_api
|
|
34
|
+
self._total_size = initial_response["totalSize"]
|
|
35
|
+
self._done = initial_response["done"]
|
|
36
|
+
self._records = initial_response["records"]
|
|
37
|
+
self._next_records_url = initial_response.get("nextRecordsUrl")
|
|
38
|
+
self._current_index = 0
|
|
39
|
+
|
|
40
|
+
def __len__(self) -> int:
|
|
41
|
+
"""Return the total number of records."""
|
|
42
|
+
return self._total_size
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def total_size(self) -> int:
|
|
46
|
+
"""Get the total number of records."""
|
|
47
|
+
return self._total_size
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def done(self) -> bool:
|
|
51
|
+
"""Check if all records have been fetched."""
|
|
52
|
+
return self._done and self._current_index >= len(self._records)
|
|
53
|
+
|
|
54
|
+
async def __aiter__(self):
|
|
55
|
+
"""Async iterator that yields individual records."""
|
|
56
|
+
# First, yield records from the initial response
|
|
57
|
+
for record in self._records[self._current_index :]:
|
|
58
|
+
self._current_index += 1
|
|
59
|
+
yield record
|
|
60
|
+
|
|
61
|
+
# Then handle pagination if needed
|
|
62
|
+
next_url = self._next_records_url
|
|
63
|
+
while next_url and not self._done:
|
|
64
|
+
# Make QueryMore request
|
|
65
|
+
more_response = await self._query_api.query_more(next_url)
|
|
66
|
+
|
|
67
|
+
# Yield records from this batch
|
|
68
|
+
for record in more_response["records"]:
|
|
69
|
+
yield record
|
|
70
|
+
|
|
71
|
+
# Update pagination state
|
|
72
|
+
self._done = more_response["done"]
|
|
73
|
+
next_url = more_response.get("nextRecordsUrl")
|
|
74
|
+
|
|
75
|
+
async def collect_all(self) -> List[Dict[str, Any]]:
|
|
76
|
+
"""Collect all records into a list."""
|
|
77
|
+
records = []
|
|
78
|
+
async for record in self:
|
|
79
|
+
records.append(record)
|
|
80
|
+
return records
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class QueryAPI:
|
|
84
|
+
"""Salesforce Query API client."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, connection: "SalesforceConnection"):
|
|
87
|
+
"""Initialize QueryAPI with a Salesforce connection."""
|
|
88
|
+
self._connection = connection
|
|
89
|
+
|
|
90
|
+
def _get_query_url(self, api_version: Optional[str] = None) -> str:
|
|
91
|
+
"""Get the base URL for query requests."""
|
|
92
|
+
version = api_version or self._connection.version
|
|
93
|
+
return f"{self._connection.instance_url}/services/data/{version}/query"
|
|
94
|
+
|
|
95
|
+
def _get_queryall_url(self, api_version: Optional[str] = None) -> str:
|
|
96
|
+
"""Get the base URL for queryAll requests (includes deleted records)."""
|
|
97
|
+
version = api_version or self._connection.version
|
|
98
|
+
return f"{self._connection.instance_url}/services/data/{version}/queryAll"
|
|
99
|
+
|
|
100
|
+
def _sanitize_soql(self, soql: str) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Basic SOQL sanitization to prevent injection attacks.
|
|
103
|
+
|
|
104
|
+
:param soql: SOQL query string
|
|
105
|
+
:returns: Sanitized SOQL string
|
|
106
|
+
:raises ValueError: If query contains potentially dangerous patterns
|
|
107
|
+
"""
|
|
108
|
+
# Remove leading/trailing whitespace
|
|
109
|
+
soql = soql.strip()
|
|
110
|
+
|
|
111
|
+
# other sanitization?
|
|
112
|
+
|
|
113
|
+
return soql
|
|
114
|
+
|
|
115
|
+
async def soql(
|
|
116
|
+
self,
|
|
117
|
+
query: str,
|
|
118
|
+
include_deleted: bool = False,
|
|
119
|
+
api_version: Optional[str] = None,
|
|
120
|
+
) -> QueryResult:
|
|
121
|
+
"""
|
|
122
|
+
Execute a SOQL query.
|
|
123
|
+
|
|
124
|
+
:param query: SOQL query string
|
|
125
|
+
:param include_deleted: If True, include deleted and archived records (uses queryAll endpoint)
|
|
126
|
+
:param api_version: API version to use
|
|
127
|
+
:returns: QueryResult that can be iterated over
|
|
128
|
+
:raises ValueError: If SOQL is invalid or potentially dangerous
|
|
129
|
+
"""
|
|
130
|
+
# Sanitize the SOQL query
|
|
131
|
+
sanitized_soql = self._sanitize_soql(query)
|
|
132
|
+
|
|
133
|
+
# Choose endpoint based on include_deleted parameter
|
|
134
|
+
if include_deleted:
|
|
135
|
+
url = self._get_queryall_url(api_version)
|
|
136
|
+
else:
|
|
137
|
+
url = self._get_query_url(api_version)
|
|
138
|
+
|
|
139
|
+
# Prepare the request
|
|
140
|
+
params = {"q": sanitized_soql}
|
|
141
|
+
|
|
142
|
+
# Make the request
|
|
143
|
+
response = await self._connection.get(url, params=params)
|
|
144
|
+
response.raise_for_status()
|
|
145
|
+
|
|
146
|
+
# Return QueryResult for iteration
|
|
147
|
+
return QueryResult(self, response.json())
|
|
148
|
+
|
|
149
|
+
async def query_more(self, next_records_url: str) -> QueryMoreResponse:
|
|
150
|
+
"""
|
|
151
|
+
Execute a QueryMore request to get the next batch of records.
|
|
152
|
+
|
|
153
|
+
:param next_records_url: The nextRecordsUrl from a previous query response
|
|
154
|
+
:returns: QueryMoreResponse with the next batch of records
|
|
155
|
+
"""
|
|
156
|
+
# The next_records_url is relative to the instance, so we need to construct the full URL
|
|
157
|
+
if next_records_url.startswith("/"):
|
|
158
|
+
url = f"{self._connection.instance_url}{next_records_url}"
|
|
159
|
+
else:
|
|
160
|
+
url = next_records_url
|
|
161
|
+
|
|
162
|
+
# Make the request
|
|
163
|
+
response = await self._connection.get(url)
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
return response.json()
|
|
166
|
+
|
|
167
|
+
async def explain(
|
|
168
|
+
self,
|
|
169
|
+
query: str,
|
|
170
|
+
api_version: Optional[str] = None,
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Get query execution plan for a SOQL query.
|
|
174
|
+
|
|
175
|
+
:param query: SOQL query string
|
|
176
|
+
:param api_version: API version to use
|
|
177
|
+
:returns: Query execution plan
|
|
178
|
+
"""
|
|
179
|
+
# Sanitize the SOQL query
|
|
180
|
+
sanitized_soql = self._sanitize_soql(query)
|
|
181
|
+
|
|
182
|
+
# Prepare the request
|
|
183
|
+
url = self._get_query_url(api_version)
|
|
184
|
+
params = {"q": sanitized_soql, "explain": "true"}
|
|
185
|
+
|
|
186
|
+
# Make the request
|
|
187
|
+
response = await self._connection.get(url, params=params)
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
return response.json()
|
|
190
|
+
|
|
191
|
+
async def sosl(
|
|
192
|
+
self,
|
|
193
|
+
search: str,
|
|
194
|
+
api_version: Optional[str] = None,
|
|
195
|
+
) -> List[Dict[str, Any]]:
|
|
196
|
+
"""
|
|
197
|
+
Execute a SOSL (Salesforce Object Search Language) search.
|
|
198
|
+
|
|
199
|
+
:param search: SOSL search string
|
|
200
|
+
:param api_version: API version to use
|
|
201
|
+
:returns: List of search results
|
|
202
|
+
"""
|
|
203
|
+
# Basic SOSL validation
|
|
204
|
+
if not search.strip().upper().startswith("FIND"):
|
|
205
|
+
raise ValueError("SOSL queries must start with FIND")
|
|
206
|
+
|
|
207
|
+
# Prepare the request
|
|
208
|
+
version = api_version or self._connection.version
|
|
209
|
+
url = f"{self._connection.instance_url}/services/data/{version}/search"
|
|
210
|
+
params = {"q": search.strip()}
|
|
211
|
+
|
|
212
|
+
# Make the request
|
|
213
|
+
response = await self._connection.get(url, params=params)
|
|
214
|
+
response.raise_for_status()
|
|
215
|
+
data = response.json()
|
|
216
|
+
return data.get("searchRecords", [])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""TypedDict definitions for Salesforce Query API responses."""
|
|
2
|
+
|
|
3
|
+
from typing import TypedDict, List, Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class QueryResponse(TypedDict):
|
|
7
|
+
"""Response from a SOQL query."""
|
|
8
|
+
|
|
9
|
+
totalSize: int
|
|
10
|
+
done: bool
|
|
11
|
+
records: List[Dict[str, Any]]
|
|
12
|
+
nextRecordsUrl: Optional[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueryAllResponse(TypedDict):
|
|
16
|
+
"""Response from a SOQL QueryAll query (includes deleted records)."""
|
|
17
|
+
|
|
18
|
+
totalSize: int
|
|
19
|
+
done: bool
|
|
20
|
+
records: List[Dict[str, Any]]
|
|
21
|
+
nextRecordsUrl: Optional[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class QueryMoreResponse(TypedDict):
|
|
25
|
+
"""Response from a QueryMore request."""
|
|
26
|
+
|
|
27
|
+
totalSize: int
|
|
28
|
+
done: bool
|
|
29
|
+
records: List[Dict[str, Any]]
|
|
30
|
+
nextRecordsUrl: Optional[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class QueryErrorResponse(TypedDict):
|
|
34
|
+
"""Error response from Query API."""
|
|
35
|
+
|
|
36
|
+
message: str
|
|
37
|
+
errorCode: str
|
|
38
|
+
fields: Optional[List[str]]
|