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,27 @@
|
|
|
1
|
+
"""aio-salesforce: Async Salesforce library for Python with Bulk API 2.0 support."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0b1"
|
|
4
|
+
__author__ = "Jonas"
|
|
5
|
+
__email__ = "charlie@callaway.cloud"
|
|
6
|
+
|
|
7
|
+
# Connection functionality
|
|
8
|
+
from .connection import ( # noqa: F401
|
|
9
|
+
SalesforceConnection,
|
|
10
|
+
SalesforceAuthError,
|
|
11
|
+
AuthStrategy,
|
|
12
|
+
ClientCredentialsAuth,
|
|
13
|
+
RefreshTokenAuth,
|
|
14
|
+
StaticTokenAuth,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Core package only exports connection functionality
|
|
18
|
+
# Users import exporter functions directly: from aio_salesforce.exporter import bulk_query
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SalesforceConnection",
|
|
22
|
+
"SalesforceAuthError",
|
|
23
|
+
"AuthStrategy",
|
|
24
|
+
"ClientCredentialsAuth",
|
|
25
|
+
"RefreshTokenAuth",
|
|
26
|
+
"StaticTokenAuth",
|
|
27
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Salesforce API Organization
|
|
2
|
+
|
|
3
|
+
This directory contains organized Salesforce API clients following a consistent convention.
|
|
4
|
+
|
|
5
|
+
## Directory Structure Convention
|
|
6
|
+
|
|
7
|
+
Each API module follows this structure:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
api/
|
|
11
|
+
├── {api_name}/
|
|
12
|
+
│ ├── __init__.py # Export API client and types
|
|
13
|
+
│ ├── client.py # Main API client class
|
|
14
|
+
│ └── types.py # TypedDict definitions for responses
|
|
15
|
+
└── __init__.py # Export all APIs and types
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Current APIs
|
|
19
|
+
|
|
20
|
+
### `describe/` - Describe API
|
|
21
|
+
- **Client**: `DescribeAPI`
|
|
22
|
+
- **Purpose**: Object describe/metadata, organization info, limits
|
|
23
|
+
- **Key Methods**:
|
|
24
|
+
- `describe_sobject()` → `SObjectDescribe`
|
|
25
|
+
- `list_sobjects()` → `List[SObjectInfo]`
|
|
26
|
+
- `get_organization_info()` → `OrganizationInfo`
|
|
27
|
+
- `get_limits()` → `OrganizationLimits`
|
|
28
|
+
|
|
29
|
+
### `bulk_v2/` - Bulk API v2
|
|
30
|
+
- **Client**: `BulkV2API`
|
|
31
|
+
- **Purpose**: Large data operations, bulk queries
|
|
32
|
+
- **Key Methods**:
|
|
33
|
+
- `create_job()` → `BulkJobInfo`
|
|
34
|
+
- `get_job_status()` → `BulkJobStatus`
|
|
35
|
+
- `get_job_results()` → `Tuple[str, Optional[str]]`
|
|
36
|
+
- `wait_for_job_completion()` → `BulkJobStatus`
|
|
37
|
+
|
|
38
|
+
### `query/` - Query API
|
|
39
|
+
- **Client**: `QueryAPI`
|
|
40
|
+
- **Purpose**: SOQL queries, QueryMore, SOSL search
|
|
41
|
+
- **Key Methods**:
|
|
42
|
+
- `soql(query, include_deleted=False)` → `QueryResult` (with async iteration)
|
|
43
|
+
- `sosl(search)` → `List[Dict[str, Any]]` (SOSL search)
|
|
44
|
+
- `explain(query)` → `Dict[str, Any]` (query execution plan)
|
|
45
|
+
- `query_more()` → `QueryMoreResponse` (internal pagination)
|
|
46
|
+
- **Features**: SOQL injection protection, automatic pagination, deleted records support
|
|
47
|
+
- **Note**: Batch size is controlled by Salesforce, not configurable in Query API
|
|
48
|
+
|
|
49
|
+
## Naming Conventions
|
|
50
|
+
|
|
51
|
+
### API Clients
|
|
52
|
+
- **Class Name**: `{ApiName}API` (e.g., `DescribeAPI`, `BulkV2API`)
|
|
53
|
+
- **File**: `client.py`
|
|
54
|
+
- **Connection Property**: `sf.{api_name}` (e.g., `sf.describe`, `sf.bulk_v2`)
|
|
55
|
+
|
|
56
|
+
### Types
|
|
57
|
+
- **File**: `types.py`
|
|
58
|
+
- **Naming**: Descriptive, specific to the API
|
|
59
|
+
- **Examples**: `SObjectDescribe`, `BulkJobInfo`, `OrganizationLimits`
|
|
60
|
+
|
|
61
|
+
### Methods
|
|
62
|
+
- **Return Types**: Always use TypedDict for structured responses
|
|
63
|
+
- **Naming**: Clear, action-oriented (e.g., `get_job_status`, `describe_sobject`)
|
|
64
|
+
- **Parameters**: Use typed parameters with Optional where appropriate
|
|
65
|
+
|
|
66
|
+
## Adding New APIs
|
|
67
|
+
|
|
68
|
+
When adding a new API (e.g., `query` for SOQL):
|
|
69
|
+
|
|
70
|
+
1. **Create directory**: `api/query/`
|
|
71
|
+
2. **Create files**:
|
|
72
|
+
```python
|
|
73
|
+
# api/query/__init__.py
|
|
74
|
+
from .client import QueryAPI
|
|
75
|
+
from .types import QueryResult, QueryError
|
|
76
|
+
__all__ = ["QueryAPI", "QueryResult", "QueryError"]
|
|
77
|
+
|
|
78
|
+
# api/query/client.py
|
|
79
|
+
class QueryAPI:
|
|
80
|
+
def __init__(self, connection): ...
|
|
81
|
+
async def execute(self, soql: str) -> QueryResult: ...
|
|
82
|
+
|
|
83
|
+
# api/query/types.py
|
|
84
|
+
class QueryResult(TypedDict): ...
|
|
85
|
+
```
|
|
86
|
+
3. **Add to main API `__init__.py`**:
|
|
87
|
+
```python
|
|
88
|
+
from .query import QueryAPI, QueryResult, QueryError
|
|
89
|
+
```
|
|
90
|
+
4. **Add to connection**:
|
|
91
|
+
```python
|
|
92
|
+
@property
|
|
93
|
+
def query(self):
|
|
94
|
+
if self._query_api is None:
|
|
95
|
+
from .api.query import QueryAPI
|
|
96
|
+
self._query_api = QueryAPI(self)
|
|
97
|
+
return self._query_api
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Benefits
|
|
101
|
+
|
|
102
|
+
- **Organization**: Each API is self-contained
|
|
103
|
+
- **Type Safety**: Full TypedDict coverage
|
|
104
|
+
- **Discoverability**: Clear structure and naming
|
|
105
|
+
- **Maintainability**: Easy to add/modify APIs
|
|
106
|
+
- **Testing**: Each API can be tested independently
|
|
107
|
+
- **Documentation**: Types serve as API documentation
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salesforce API modules.
|
|
3
|
+
|
|
4
|
+
This package provides organized access to Salesforce APIs:
|
|
5
|
+
- describe: Object and organization describe/metadata
|
|
6
|
+
- bulk_v2: Bulk API v2 for large data operations
|
|
7
|
+
- query: SOQL queries and QueryMore operations
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Import API clients and types from organized submodules
|
|
11
|
+
from .bulk_v2 import (
|
|
12
|
+
BulkV2API,
|
|
13
|
+
BulkJobCreateRequest,
|
|
14
|
+
BulkJobInfo,
|
|
15
|
+
BulkJobStatus,
|
|
16
|
+
BulkJobError,
|
|
17
|
+
)
|
|
18
|
+
from .describe import (
|
|
19
|
+
DescribeAPI,
|
|
20
|
+
FieldInfo,
|
|
21
|
+
LimitInfo,
|
|
22
|
+
OrganizationInfo,
|
|
23
|
+
OrganizationLimits,
|
|
24
|
+
PicklistValue,
|
|
25
|
+
RecordTypeInfo,
|
|
26
|
+
SObjectDescribe,
|
|
27
|
+
SObjectInfo,
|
|
28
|
+
SalesforceAttributes,
|
|
29
|
+
)
|
|
30
|
+
from .query import (
|
|
31
|
+
QueryAPI,
|
|
32
|
+
QueryResult,
|
|
33
|
+
QueryResponse,
|
|
34
|
+
QueryAllResponse,
|
|
35
|
+
QueryMoreResponse,
|
|
36
|
+
QueryErrorResponse,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# API Clients
|
|
41
|
+
"BulkV2API",
|
|
42
|
+
"DescribeAPI",
|
|
43
|
+
"QueryAPI",
|
|
44
|
+
# Bulk v2 Types
|
|
45
|
+
"BulkJobCreateRequest",
|
|
46
|
+
"BulkJobInfo",
|
|
47
|
+
"BulkJobStatus",
|
|
48
|
+
"BulkJobError",
|
|
49
|
+
# Describe Types
|
|
50
|
+
"FieldInfo",
|
|
51
|
+
"LimitInfo",
|
|
52
|
+
"OrganizationInfo",
|
|
53
|
+
"OrganizationLimits",
|
|
54
|
+
"PicklistValue",
|
|
55
|
+
"RecordTypeInfo",
|
|
56
|
+
"SObjectDescribe",
|
|
57
|
+
"SObjectInfo",
|
|
58
|
+
"SalesforceAttributes",
|
|
59
|
+
# Query Types
|
|
60
|
+
"QueryResult",
|
|
61
|
+
"QueryResponse",
|
|
62
|
+
"QueryAllResponse",
|
|
63
|
+
"QueryMoreResponse",
|
|
64
|
+
"QueryErrorResponse",
|
|
65
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salesforce Bulk API v2.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import BulkV2API
|
|
6
|
+
from .types import (
|
|
7
|
+
BulkJobCreateRequest,
|
|
8
|
+
BulkJobInfo,
|
|
9
|
+
BulkJobStatus,
|
|
10
|
+
BulkJobError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# API Client
|
|
15
|
+
"BulkV2API",
|
|
16
|
+
# Types
|
|
17
|
+
"BulkJobCreateRequest",
|
|
18
|
+
"BulkJobInfo",
|
|
19
|
+
"BulkJobStatus",
|
|
20
|
+
"BulkJobError",
|
|
21
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salesforce Bulk API v2 methods.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional, Tuple, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .types import (
|
|
11
|
+
BulkJobCreateRequest,
|
|
12
|
+
BulkJobInfo,
|
|
13
|
+
BulkJobStatus,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ...connection import SalesforceConnection
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BulkV2API:
|
|
23
|
+
"""Salesforce Bulk API v2 methods."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, connection: "SalesforceConnection"):
|
|
26
|
+
self.connection = connection
|
|
27
|
+
|
|
28
|
+
def _get_base_url(self, api_version: Optional[str] = None) -> str:
|
|
29
|
+
"""Get the base URL for Bulk API v2 requests."""
|
|
30
|
+
return self.connection.get_base_url(api_version)
|
|
31
|
+
|
|
32
|
+
def _get_job_url(self, job_id: str, api_version: Optional[str] = None) -> str:
|
|
33
|
+
"""Get the URL for a specific bulk job."""
|
|
34
|
+
base_url = self._get_base_url(api_version)
|
|
35
|
+
return f"{base_url}/jobs/query/{job_id}"
|
|
36
|
+
|
|
37
|
+
def _get_job_results_url(
|
|
38
|
+
self, job_id: str, api_version: Optional[str] = None
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Get the URL for fetching bulk job results."""
|
|
41
|
+
base_url = self._get_base_url(api_version)
|
|
42
|
+
return f"{base_url}/jobs/query/{job_id}/results"
|
|
43
|
+
|
|
44
|
+
def _get_jobs_url(self, api_version: Optional[str] = None) -> str:
|
|
45
|
+
"""Get the URL for creating bulk jobs."""
|
|
46
|
+
base_url = self._get_base_url(api_version)
|
|
47
|
+
return f"{base_url}/jobs/query"
|
|
48
|
+
|
|
49
|
+
async def create_job(
|
|
50
|
+
self,
|
|
51
|
+
soql_query: str,
|
|
52
|
+
all_rows: bool = False,
|
|
53
|
+
api_version: Optional[str] = None,
|
|
54
|
+
) -> BulkJobInfo:
|
|
55
|
+
"""
|
|
56
|
+
Create a new bulk query job.
|
|
57
|
+
|
|
58
|
+
:param soql_query: The SOQL query to execute
|
|
59
|
+
:param all_rows: If True, includes deleted and archived records (queryAll)
|
|
60
|
+
:param api_version: API version to use (defaults to connection version)
|
|
61
|
+
:returns: Job information
|
|
62
|
+
"""
|
|
63
|
+
job_url = self._get_jobs_url(api_version)
|
|
64
|
+
job_data: BulkJobCreateRequest = {
|
|
65
|
+
"operation": "queryAll" if all_rows else "query",
|
|
66
|
+
"query": soql_query,
|
|
67
|
+
"contentType": "CSV",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
response = await self.connection.post(job_url, json=job_data)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
job_info = response.json()
|
|
73
|
+
|
|
74
|
+
logger.info(
|
|
75
|
+
f"Created bulk job {job_info['id']} for query: {soql_query[:100]}..."
|
|
76
|
+
)
|
|
77
|
+
return job_info
|
|
78
|
+
|
|
79
|
+
async def get_job_status(
|
|
80
|
+
self,
|
|
81
|
+
job_id: str,
|
|
82
|
+
api_version: Optional[str] = None,
|
|
83
|
+
) -> BulkJobStatus:
|
|
84
|
+
"""
|
|
85
|
+
Get the status of a bulk job.
|
|
86
|
+
|
|
87
|
+
:param job_id: The job ID
|
|
88
|
+
:param api_version: API version to use (defaults to connection version)
|
|
89
|
+
:returns: Job status information
|
|
90
|
+
"""
|
|
91
|
+
status_url = self._get_job_url(job_id, api_version)
|
|
92
|
+
response = await self.connection.get(status_url)
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
return response.json()
|
|
95
|
+
|
|
96
|
+
async def get_job_results(
|
|
97
|
+
self,
|
|
98
|
+
job_id: str,
|
|
99
|
+
locator: Optional[str] = None,
|
|
100
|
+
max_records: int = 10000,
|
|
101
|
+
api_version: Optional[str] = None,
|
|
102
|
+
) -> Tuple[str, Optional[str]]:
|
|
103
|
+
"""
|
|
104
|
+
Get results from a completed bulk job.
|
|
105
|
+
|
|
106
|
+
:param job_id: The job ID
|
|
107
|
+
:param locator: Query locator for pagination (optional)
|
|
108
|
+
:param max_records: Maximum number of records to fetch
|
|
109
|
+
:param api_version: API version to use (defaults to connection version)
|
|
110
|
+
:returns: Tuple of (CSV response text, next locator or None)
|
|
111
|
+
"""
|
|
112
|
+
results_url = self._get_job_results_url(job_id, api_version)
|
|
113
|
+
params = {"maxRecords": max_records}
|
|
114
|
+
if locator:
|
|
115
|
+
params["locator"] = locator
|
|
116
|
+
|
|
117
|
+
response = await self.connection.get(results_url, params=params)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
|
|
120
|
+
# Get next locator from headers
|
|
121
|
+
next_locator = response.headers.get("Sforce-Locator")
|
|
122
|
+
if next_locator == "null":
|
|
123
|
+
next_locator = None
|
|
124
|
+
|
|
125
|
+
return response.text, next_locator
|
|
126
|
+
|
|
127
|
+
async def wait_for_job_completion(
|
|
128
|
+
self,
|
|
129
|
+
job_id: str,
|
|
130
|
+
poll_interval: int = 5,
|
|
131
|
+
timeout: Optional[int] = None,
|
|
132
|
+
api_version: Optional[str] = None,
|
|
133
|
+
) -> BulkJobStatus:
|
|
134
|
+
"""
|
|
135
|
+
Wait for a bulk job to complete.
|
|
136
|
+
|
|
137
|
+
:param job_id: The job ID to monitor
|
|
138
|
+
:param poll_interval: Time in seconds between status checks
|
|
139
|
+
:param timeout: Maximum time to wait (None for no timeout)
|
|
140
|
+
:param api_version: API version to use (defaults to connection version)
|
|
141
|
+
:returns: Final job status
|
|
142
|
+
:raises TimeoutError: If job doesn't complete within timeout
|
|
143
|
+
:raises Exception: If job fails
|
|
144
|
+
"""
|
|
145
|
+
logger.info(f"Waiting for job {job_id} to complete...")
|
|
146
|
+
start_time = time.time()
|
|
147
|
+
|
|
148
|
+
while True:
|
|
149
|
+
job_status = await self.get_job_status(job_id, api_version)
|
|
150
|
+
state = job_status["state"]
|
|
151
|
+
|
|
152
|
+
if state == "JobComplete":
|
|
153
|
+
total_records = job_status.get("numberRecordsProcessed", 0)
|
|
154
|
+
logger.info(
|
|
155
|
+
f"Job {job_id} completed successfully. Total records: {total_records}"
|
|
156
|
+
)
|
|
157
|
+
return job_status
|
|
158
|
+
elif state == "Failed":
|
|
159
|
+
raise Exception(f"Job {job_id} failed: {job_status}")
|
|
160
|
+
else:
|
|
161
|
+
# Check timeout
|
|
162
|
+
if timeout and (time.time() - start_time) > timeout:
|
|
163
|
+
raise TimeoutError(
|
|
164
|
+
f"Job {job_id} did not complete within {timeout} seconds"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Job is still running
|
|
168
|
+
logger.debug(
|
|
169
|
+
f"Job {job_id} state: {state}, waiting {poll_interval}s..."
|
|
170
|
+
)
|
|
171
|
+
await asyncio.sleep(poll_interval)
|
|
172
|
+
|
|
173
|
+
async def execute_query(
|
|
174
|
+
self,
|
|
175
|
+
soql_query: str,
|
|
176
|
+
all_rows: bool = False,
|
|
177
|
+
poll_interval: int = 5,
|
|
178
|
+
timeout: Optional[int] = None,
|
|
179
|
+
api_version: Optional[str] = None,
|
|
180
|
+
) -> BulkJobStatus:
|
|
181
|
+
"""
|
|
182
|
+
Execute a SOQL query via Bulk API v2 and wait for completion.
|
|
183
|
+
|
|
184
|
+
This is a convenience method that creates a job and waits for it to complete.
|
|
185
|
+
|
|
186
|
+
:param soql_query: The SOQL query to execute
|
|
187
|
+
:param all_rows: If True, includes deleted and archived records
|
|
188
|
+
:param poll_interval: Time in seconds between status checks
|
|
189
|
+
:param timeout: Maximum time to wait (None for no timeout)
|
|
190
|
+
:param api_version: API version to use (defaults to connection version)
|
|
191
|
+
:returns: Final job status
|
|
192
|
+
"""
|
|
193
|
+
# Create the job
|
|
194
|
+
job_info = await self.create_job(soql_query, all_rows, api_version)
|
|
195
|
+
job_id = job_info["id"]
|
|
196
|
+
|
|
197
|
+
# Wait for completion
|
|
198
|
+
return await self.wait_for_job_completion(
|
|
199
|
+
job_id, poll_interval, timeout, api_version
|
|
200
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypedDict definitions for Salesforce Bulk API v2 responses.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BulkJobInfo(TypedDict):
|
|
9
|
+
"""Bulk job information."""
|
|
10
|
+
|
|
11
|
+
id: str
|
|
12
|
+
operation: str
|
|
13
|
+
object: str
|
|
14
|
+
createdById: str
|
|
15
|
+
createdDate: str
|
|
16
|
+
systemModstamp: str
|
|
17
|
+
state: str
|
|
18
|
+
concurrencyMode: str
|
|
19
|
+
contentType: str
|
|
20
|
+
apiVersion: str
|
|
21
|
+
jobType: str
|
|
22
|
+
lineEnding: str
|
|
23
|
+
columnDelimiter: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BulkJobCreateRequest(TypedDict):
|
|
27
|
+
"""Request payload for creating a bulk job."""
|
|
28
|
+
|
|
29
|
+
operation: str
|
|
30
|
+
query: str
|
|
31
|
+
contentType: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BulkJobStatus(TypedDict):
|
|
35
|
+
"""Bulk job status information."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
operation: str
|
|
39
|
+
object: str
|
|
40
|
+
createdById: str
|
|
41
|
+
createdDate: str
|
|
42
|
+
systemModstamp: str
|
|
43
|
+
state: str
|
|
44
|
+
concurrencyMode: str
|
|
45
|
+
contentType: str
|
|
46
|
+
apiVersion: str
|
|
47
|
+
jobType: str
|
|
48
|
+
lineEnding: str
|
|
49
|
+
columnDelimiter: str
|
|
50
|
+
numberBatchesQueued: int
|
|
51
|
+
numberBatchesInProgress: int
|
|
52
|
+
numberBatchesCompleted: int
|
|
53
|
+
numberBatchesFailed: int
|
|
54
|
+
numberBatchesTotal: int
|
|
55
|
+
numberRequestsCompleted: int
|
|
56
|
+
numberRequestsFailed: int
|
|
57
|
+
numberRequestsTotal: int
|
|
58
|
+
numberRecordsProcessed: int
|
|
59
|
+
numberRecordsFailed: int
|
|
60
|
+
numberRetries: int
|
|
61
|
+
apiActiveProcessingTime: int
|
|
62
|
+
apexProcessingTime: int
|
|
63
|
+
totalProcessingTime: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BulkJobError(TypedDict):
|
|
67
|
+
"""Bulk job error information."""
|
|
68
|
+
|
|
69
|
+
message: str
|
|
70
|
+
errorCode: str
|
|
71
|
+
fields: List[str]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salesforce Describe API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import DescribeAPI
|
|
6
|
+
from .types import (
|
|
7
|
+
FieldInfo,
|
|
8
|
+
LimitInfo,
|
|
9
|
+
OrganizationInfo,
|
|
10
|
+
OrganizationLimits,
|
|
11
|
+
PicklistValue,
|
|
12
|
+
RecordTypeInfo,
|
|
13
|
+
SObjectDescribe,
|
|
14
|
+
SObjectInfo,
|
|
15
|
+
SalesforceAttributes,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# API Client
|
|
20
|
+
"DescribeAPI",
|
|
21
|
+
# Types
|
|
22
|
+
"FieldInfo",
|
|
23
|
+
"LimitInfo",
|
|
24
|
+
"OrganizationInfo",
|
|
25
|
+
"OrganizationLimits",
|
|
26
|
+
"PicklistValue",
|
|
27
|
+
"RecordTypeInfo",
|
|
28
|
+
"SObjectDescribe",
|
|
29
|
+
"SObjectInfo",
|
|
30
|
+
"SalesforceAttributes",
|
|
31
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Salesforce Describe API methods.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
OrganizationInfo,
|
|
10
|
+
OrganizationLimits,
|
|
11
|
+
SObjectDescribe,
|
|
12
|
+
SObjectInfo,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ...connection import SalesforceConnection
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DescribeAPI:
|
|
22
|
+
"""Salesforce Describe API methods."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, connection: "SalesforceConnection"):
|
|
25
|
+
self.connection = connection
|
|
26
|
+
|
|
27
|
+
async def sobject(
|
|
28
|
+
self, sobject_type: str, api_version: Optional[str] = None
|
|
29
|
+
) -> SObjectDescribe:
|
|
30
|
+
"""
|
|
31
|
+
Get metadata for a Salesforce object.
|
|
32
|
+
|
|
33
|
+
:param sobject_type: Name of the Salesforce object (e.g., 'Account', 'Contact')
|
|
34
|
+
:param api_version: API version to use (defaults to connection version)
|
|
35
|
+
:returns: Object metadata dictionary
|
|
36
|
+
"""
|
|
37
|
+
url = self.connection.get_describe_url(sobject_type, api_version)
|
|
38
|
+
response = await self.connection.get(url)
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
return response.json()
|
|
41
|
+
|
|
42
|
+
async def list_sobjects(
|
|
43
|
+
self, api_version: Optional[str] = None
|
|
44
|
+
) -> List[SObjectInfo]:
|
|
45
|
+
"""
|
|
46
|
+
List all available Salesforce objects.
|
|
47
|
+
|
|
48
|
+
:param api_version: API version to use (defaults to connection version)
|
|
49
|
+
:returns: List of object metadata dictionaries
|
|
50
|
+
"""
|
|
51
|
+
base_url = self.connection.get_base_url(api_version)
|
|
52
|
+
url = f"{base_url}/sobjects"
|
|
53
|
+
response = await self.connection.get(url)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
data = response.json()
|
|
56
|
+
return data.get("sobjects", [])
|
|
57
|
+
|
|
58
|
+
async def get_limits(self, api_version: Optional[str] = None) -> OrganizationLimits:
|
|
59
|
+
"""
|
|
60
|
+
Get organization limits.
|
|
61
|
+
|
|
62
|
+
:param api_version: API version to use (defaults to connection version)
|
|
63
|
+
:returns: Organization limits dictionary
|
|
64
|
+
"""
|
|
65
|
+
base_url = self.connection.get_base_url(api_version)
|
|
66
|
+
url = f"{base_url}/limits"
|
|
67
|
+
response = await self.connection.get(url)
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
return response.json()
|
|
70
|
+
|
|
71
|
+
async def get_organization_info(
|
|
72
|
+
self, api_version: Optional[str] = None
|
|
73
|
+
) -> OrganizationInfo:
|
|
74
|
+
"""
|
|
75
|
+
Get organization information.
|
|
76
|
+
|
|
77
|
+
:param api_version: API version to use (defaults to connection version)
|
|
78
|
+
:returns: Organization info dictionary
|
|
79
|
+
"""
|
|
80
|
+
# Query the Organization object for basic org info
|
|
81
|
+
base_url = self.connection.get_base_url(api_version)
|
|
82
|
+
url = f"{base_url}/query"
|
|
83
|
+
params = {
|
|
84
|
+
"q": "SELECT Id, Name, OrganizationType, InstanceName, IsSandbox FROM Organization LIMIT 1"
|
|
85
|
+
}
|
|
86
|
+
response = await self.connection.get(url, params=params)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
data = response.json()
|
|
89
|
+
|
|
90
|
+
records = data.get("records", [])
|
|
91
|
+
if records:
|
|
92
|
+
return records[0]
|
|
93
|
+
else:
|
|
94
|
+
raise RuntimeError("Unable to retrieve organization information")
|