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.
@@ -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]]