Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -15,8 +15,8 @@ from utils import extract_kyc_fields
|
|
15 |
SF_USERNAME = "[email protected]"
|
16 |
SF_PASSWORD = "Lic@2025"
|
17 |
SF_SECURITY_TOKEN = "AmmfRcd6IiYaRtSGntBnzNMQU"
|
18 |
-
SF_DOMAIN = "login" # "login" for prod, "test" for sandbox
|
19 |
-
SF_API_VERSION = "60.0" #
|
20 |
# -------------------------------------------------------------------------------
|
21 |
|
22 |
|
@@ -57,14 +57,12 @@ def _parse_birthdate(dob_text: str):
|
|
57 |
return None
|
58 |
|
59 |
|
60 |
-
def
|
61 |
"""
|
62 |
-
|
63 |
-
Returns (instance_url,
|
64 |
"""
|
65 |
endpoint = f"https://{domain}.salesforce.com/services/Soap/u/{api_version}"
|
66 |
-
combined_password = f"{password}{token}"
|
67 |
-
|
68 |
envelope = f"""<?xml version="1.0" encoding="utf-8" ?>
|
69 |
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
70 |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
@@ -72,7 +70,7 @@ def _soap_login(username: str, password: str, token: str, domain: str = "login",
|
|
72 |
<env:Body>
|
73 |
<n1:login xmlns:n1="urn:partner.soap.sforce.com">
|
74 |
<n1:username>{username}</n1:username>
|
75 |
-
<n1:password>{
|
76 |
</n1:login>
|
77 |
</env:Body>
|
78 |
</env:Envelope>"""
|
@@ -84,7 +82,12 @@ def _soap_login(username: str, password: str, token: str, domain: str = "login",
|
|
84 |
|
85 |
resp = requests.post(endpoint, data=envelope.encode("utf-8"), headers=headers, timeout=30)
|
86 |
if resp.status_code != 200:
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
88 |
|
89 |
# Parse XML to find sessionId and serverUrl
|
90 |
try:
|
@@ -92,7 +95,6 @@ def _soap_login(username: str, password: str, token: str, domain: str = "login",
|
|
92 |
except ET.ParseError as e:
|
93 |
raise RuntimeError(f"SOAP login parse error: {e}")
|
94 |
|
95 |
-
# Find tags by localname to avoid namespace headaches
|
96 |
session_id = None
|
97 |
server_url = None
|
98 |
for elem in root.iter():
|
@@ -103,16 +105,68 @@ def _soap_login(username: str, password: str, token: str, domain: str = "login",
|
|
103 |
server_url = elem.text
|
104 |
|
105 |
if not session_id or not server_url:
|
106 |
-
|
|
|
107 |
|
108 |
parsed = urlparse(server_url)
|
109 |
instance_url = f"{parsed.scheme}://{parsed.netloc}"
|
110 |
return instance_url, session_id
|
111 |
|
112 |
|
113 |
-
def
|
114 |
"""
|
115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
"""
|
117 |
url = f"{instance_url}/services/data/v{api_version}/sobjects/{object_api}"
|
118 |
headers = {
|
@@ -130,7 +184,6 @@ def _rest_create(instance_url: str, access_token: str, object_api: str, payload:
|
|
130 |
rec_id = body.get("id") if isinstance(body, dict) else None
|
131 |
return {"success": True, "id": rec_id, "status_code": resp.status_code, "url": url, "response_json": body}
|
132 |
else:
|
133 |
-
# Salesforce error lists come back as JSON arrays usually
|
134 |
return {
|
135 |
"success": False,
|
136 |
"status_code": resp.status_code,
|
@@ -143,7 +196,7 @@ def _rest_create(instance_url: str, access_token: str, object_api: str, payload:
|
|
143 |
def sf_push_kyc_record(ocr_results: dict):
|
144 |
"""
|
145 |
Combine Aadhaar + PAN into one KYC_Record__c and create via REST.
|
146 |
-
Fields expected
|
147 |
Aadhaar_Number__c, Aadhaar_Name__c, Aadhaar_DOB__c (Date)
|
148 |
PAN_Number__c, Pan_Name__c, Pan_DOB__c (Date)
|
149 |
"""
|
@@ -168,17 +221,21 @@ def sf_push_kyc_record(ocr_results: dict):
|
|
168 |
}
|
169 |
payload = {k: v for k, v in payload.items() if v is not None}
|
170 |
|
171 |
-
# 1) SOAP login
|
172 |
-
instance_url, access_token =
|
|
|
|
|
173 |
# 2) REST create
|
174 |
-
|
|
|
|
|
175 |
|
176 |
|
177 |
# ---------- gradio callback ----------
|
178 |
def process_documents(aadhaar_file, pan_file, push_to_sf):
|
179 |
"""
|
180 |
- Runs OCR on Aadhaar and PAN separately.
|
181 |
-
- Optionally pushes a single KYC_Record__c to Salesforce via SOAP+REST
|
182 |
"""
|
183 |
results = {"aadhaar": None, "pan": None}
|
184 |
|
@@ -210,7 +267,10 @@ def process_documents(aadhaar_file, pan_file, push_to_sf):
|
|
210 |
created = sf_push_kyc_record(results)
|
211 |
output["salesforce"] = {"pushed": created.get("success", False), **created}
|
212 |
except Exception as e:
|
213 |
-
output["salesforce"] = {
|
|
|
|
|
|
|
214 |
|
215 |
return output
|
216 |
|
@@ -250,10 +310,11 @@ with gr.Blocks(title="Smart KYC OCR → Salesforce (KYC_Record__c)") as demo:
|
|
250 |
gr.Markdown("---")
|
251 |
gr.Markdown(
|
252 |
"""
|
253 |
-
|
|
|
254 |
"""
|
255 |
)
|
256 |
|
257 |
-
# Keep `demo`
|
258 |
if __name__ == "__main__":
|
259 |
demo.launch()
|
|
|
15 |
SF_USERNAME = "[email protected]"
|
16 |
SF_PASSWORD = "Lic@2025"
|
17 |
SF_SECURITY_TOKEN = "AmmfRcd6IiYaRtSGntBnzNMQU"
|
18 |
+
SF_DOMAIN = "login" # "login" for prod, "test" for sandbox (we will auto-try both)
|
19 |
+
SF_API_VERSION = "60.0" # REST & Partner SOAP API version
|
20 |
# -------------------------------------------------------------------------------
|
21 |
|
22 |
|
|
|
57 |
return None
|
58 |
|
59 |
|
60 |
+
def _soap_login_once(username: str, password: str, domain: str, api_version: str):
|
61 |
"""
|
62 |
+
One SOAP login attempt with given password (already concatenated or not).
|
63 |
+
Returns (instance_url, session_id) on success; raises RuntimeError on failure with fault details.
|
64 |
"""
|
65 |
endpoint = f"https://{domain}.salesforce.com/services/Soap/u/{api_version}"
|
|
|
|
|
66 |
envelope = f"""<?xml version="1.0" encoding="utf-8" ?>
|
67 |
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
68 |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
|
70 |
<env:Body>
|
71 |
<n1:login xmlns:n1="urn:partner.soap.sforce.com">
|
72 |
<n1:username>{username}</n1:username>
|
73 |
+
<n1:password>{password}</n1:password>
|
74 |
</n1:login>
|
75 |
</env:Body>
|
76 |
</env:Envelope>"""
|
|
|
82 |
|
83 |
resp = requests.post(endpoint, data=envelope.encode("utf-8"), headers=headers, timeout=30)
|
84 |
if resp.status_code != 200:
|
85 |
+
# Try to parse a SOAP fault if present
|
86 |
+
faultcode, faultstring = _extract_fault(resp.text)
|
87 |
+
msg = f"HTTP {resp.status_code}"
|
88 |
+
if faultcode or faultstring:
|
89 |
+
msg += f" | faultcode={faultcode} | faultstring={faultstring}"
|
90 |
+
raise RuntimeError(msg)
|
91 |
|
92 |
# Parse XML to find sessionId and serverUrl
|
93 |
try:
|
|
|
95 |
except ET.ParseError as e:
|
96 |
raise RuntimeError(f"SOAP login parse error: {e}")
|
97 |
|
|
|
98 |
session_id = None
|
99 |
server_url = None
|
100 |
for elem in root.iter():
|
|
|
105 |
server_url = elem.text
|
106 |
|
107 |
if not session_id or not server_url:
|
108 |
+
faultcode, faultstring = _extract_fault(resp.text)
|
109 |
+
raise RuntimeError(f"SOAP login failed: sessionId/serverUrl not found | faultcode={faultcode} | faultstring={faultstring}")
|
110 |
|
111 |
parsed = urlparse(server_url)
|
112 |
instance_url = f"{parsed.scheme}://{parsed.netloc}"
|
113 |
return instance_url, session_id
|
114 |
|
115 |
|
116 |
+
def _extract_fault(xml_text: str):
|
117 |
"""
|
118 |
+
Extract SOAP faultcode and faultstring for clearer errors.
|
119 |
+
"""
|
120 |
+
try:
|
121 |
+
root = ET.fromstring(xml_text)
|
122 |
+
except Exception:
|
123 |
+
return None, None
|
124 |
+
faultcode = None
|
125 |
+
faultstring = None
|
126 |
+
for elem in root.iter():
|
127 |
+
tag = elem.tag.split('}', 1)[-1]
|
128 |
+
if tag == "faultcode":
|
129 |
+
faultcode = elem.text
|
130 |
+
elif tag == "faultstring":
|
131 |
+
faultstring = elem.text
|
132 |
+
return faultcode, faultstring
|
133 |
+
|
134 |
+
|
135 |
+
def soap_login_all_paths(username: str, password: str, token: str, preferred_domain: str = "login", api_version: str = SF_API_VERSION):
|
136 |
+
"""
|
137 |
+
Try multiple safe login permutations:
|
138 |
+
1) preferred_domain with password+token
|
139 |
+
2) preferred_domain with password only
|
140 |
+
3) alternate domain with password+token
|
141 |
+
4) alternate domain with password only
|
142 |
+
|
143 |
+
Returns (instance_url, session_id, diagnostics) on success.
|
144 |
+
On failure, raises RuntimeError with aggregated diagnostics.
|
145 |
+
"""
|
146 |
+
domains = [preferred_domain] + [d for d in ["login", "test"] if d != preferred_domain]
|
147 |
+
attempts = []
|
148 |
+
for domain in domains:
|
149 |
+
for mode in ["pw_token", "pw_only"]:
|
150 |
+
pw = f"{password}{token}" if mode == "pw_token" else password
|
151 |
+
try:
|
152 |
+
instance_url, session_id = _soap_login_once(username, pw, domain, api_version)
|
153 |
+
diag = {"domain": domain, "mode": mode, "result": "success"}
|
154 |
+
attempts.append(diag)
|
155 |
+
return instance_url, session_id, attempts
|
156 |
+
except Exception as e:
|
157 |
+
# Collect reason but keep trying
|
158 |
+
attempts.append({"domain": domain, "mode": mode, "result": "fail", "reason": str(e)})
|
159 |
+
|
160 |
+
# If we’re here, all attempts failed
|
161 |
+
raise RuntimeError(json.dumps({
|
162 |
+
"message": "All SOAP login attempts failed",
|
163 |
+
"attempts": attempts
|
164 |
+
}))
|
165 |
+
|
166 |
+
|
167 |
+
def rest_create(instance_url: str, access_token: str, object_api: str, payload: dict, api_version: str = SF_API_VERSION):
|
168 |
+
"""
|
169 |
+
Create a record via REST API. Returns dict with success flag and full details.
|
170 |
"""
|
171 |
url = f"{instance_url}/services/data/v{api_version}/sobjects/{object_api}"
|
172 |
headers = {
|
|
|
184 |
rec_id = body.get("id") if isinstance(body, dict) else None
|
185 |
return {"success": True, "id": rec_id, "status_code": resp.status_code, "url": url, "response_json": body}
|
186 |
else:
|
|
|
187 |
return {
|
188 |
"success": False,
|
189 |
"status_code": resp.status_code,
|
|
|
196 |
def sf_push_kyc_record(ocr_results: dict):
|
197 |
"""
|
198 |
Combine Aadhaar + PAN into one KYC_Record__c and create via REST.
|
199 |
+
Fields expected (API names):
|
200 |
Aadhaar_Number__c, Aadhaar_Name__c, Aadhaar_DOB__c (Date)
|
201 |
PAN_Number__c, Pan_Name__c, Pan_DOB__c (Date)
|
202 |
"""
|
|
|
221 |
}
|
222 |
payload = {k: v for k, v in payload.items() if v is not None}
|
223 |
|
224 |
+
# 1) SOAP login (auto-tries domains & password modes)
|
225 |
+
instance_url, access_token, diagnostics = soap_login_all_paths(
|
226 |
+
SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN, preferred_domain=SF_DOMAIN, api_version=SF_API_VERSION
|
227 |
+
)
|
228 |
# 2) REST create
|
229 |
+
rest_res = rest_create(instance_url, access_token, "KYC_Record__c", payload, SF_API_VERSION)
|
230 |
+
rest_res["login_diagnostics"] = diagnostics
|
231 |
+
return rest_res
|
232 |
|
233 |
|
234 |
# ---------- gradio callback ----------
|
235 |
def process_documents(aadhaar_file, pan_file, push_to_sf):
|
236 |
"""
|
237 |
- Runs OCR on Aadhaar and PAN separately.
|
238 |
+
- Optionally pushes a single KYC_Record__c to Salesforce via SOAP+REST.
|
239 |
"""
|
240 |
results = {"aadhaar": None, "pan": None}
|
241 |
|
|
|
267 |
created = sf_push_kyc_record(results)
|
268 |
output["salesforce"] = {"pushed": created.get("success", False), **created}
|
269 |
except Exception as e:
|
270 |
+
output["salesforce"] = {
|
271 |
+
"pushed": False,
|
272 |
+
"error": {"type": e.__class__.__name__, "message": str(e)}
|
273 |
+
}
|
274 |
|
275 |
return output
|
276 |
|
|
|
310 |
gr.Markdown("---")
|
311 |
gr.Markdown(
|
312 |
"""
|
313 |
+
On failure you’ll see a `login_diagnostics` list (each attempt with domain+mode+reason)
|
314 |
+
and REST `status_code/url/response_json`. This eliminates the SDK TypeError and pinpoints issues fast.
|
315 |
"""
|
316 |
)
|
317 |
|
318 |
+
# Keep `demo` available for local run / Spaces
|
319 |
if __name__ == "__main__":
|
320 |
demo.launch()
|