Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import requests # For making HTTP requests to Salesforce
|
| 5 |
+
from model import process_vendor_logs # Import your scoring logic from model.py
|
| 6 |
+
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
|
| 9 |
+
# --- Configuration ---
|
| 10 |
+
# These should be set as environment variables in your Hugging Face Space for security
|
| 11 |
+
SALESFORCE_CONSUMER_KEY = os.environ.get('SALESFORCE_CONSUMER_KEY')
|
| 12 |
+
SALESFORCE_CONSUMER_SECRET = os.environ.get('SALESFORCE_CONSUMER_SECRET')
|
| 13 |
+
SALESFORCE_CALLBACK_URL = os.environ.get('SALESFORCE_CALLBACK_URL') # Needs to match your Connected App
|
| 14 |
+
SALESFORCE_TOKEN_URL = "https://login.salesforce.com/services/oauth2/token" # Or your sandbox URL
|
| 15 |
+
SALESFORCE_AUTH_URL = "https://login.salesforce.com/services/oauth2/authorize" # Or your sandbox URL
|
| 16 |
+
|
| 17 |
+
# Placeholder for your Salesforce instance URL. This will be obtained dynamically
|
| 18 |
+
SALESFORCE_INSTANCE_URL = None
|
| 19 |
+
|
| 20 |
+
# Hugging Face Space doesn't persist data, so use a very basic in-memory storage for demonstration.
|
| 21 |
+
# Replace with a database (e.g., PostgreSQL) for a production environment.
|
| 22 |
+
auth_code = None
|
| 23 |
+
access_token = None
|
| 24 |
+
refresh_token = None
|
| 25 |
+
|
| 26 |
+
# --- Helper Functions ---
|
| 27 |
+
|
| 28 |
+
def get_salesforce_access_token(code):
|
| 29 |
+
"""
|
| 30 |
+
Exchanges an authorization code for an access token and refresh token from Salesforce.
|
| 31 |
+
Args:
|
| 32 |
+
code: The authorization code received from Salesforce.
|
| 33 |
+
Returns:
|
| 34 |
+
A tuple containing the access token and refresh token, or (None, None) on error.
|
| 35 |
+
"""
|
| 36 |
+
token_data = {
|
| 37 |
+
'grant_type': 'authorization_code',
|
| 38 |
+
'code': code,
|
| 39 |
+
'client_id': SALESFORCE_CONSUMER_KEY,
|
| 40 |
+
'client_secret': SALESFORCE_CONSUMER_SECRET,
|
| 41 |
+
'redirect_uri': SALESFORCE_CALLBACK_URL
|
| 42 |
+
}
|
| 43 |
+
response = requests.post(SALESFORCE_TOKEN_URL, data=token_data)
|
| 44 |
+
if response.status_code == 200:
|
| 45 |
+
token_response = response.json()
|
| 46 |
+
return token_response.get('access_token'), token_response.get('refresh_token'), token_response.get('instance_url')
|
| 47 |
+
else:
|
| 48 |
+
print(f"Error getting access token: {response.text}") # Log the error
|
| 49 |
+
return None, None, None
|
| 50 |
+
|
| 51 |
+
def refresh_salesforce_access_token(refresh_token):
|
| 52 |
+
"""
|
| 53 |
+
Refreshes the access token using the refresh token.
|
| 54 |
+
Args:
|
| 55 |
+
refresh_token: The refresh token from Salesforce.
|
| 56 |
+
Returns:
|
| 57 |
+
A tuple containing the new access token and refresh token, or (None, None) on error.
|
| 58 |
+
"""
|
| 59 |
+
token_data = {
|
| 60 |
+
'grant_type': 'refresh_token',
|
| 61 |
+
'refresh_token': refresh_token,
|
| 62 |
+
'client_id': SALESFORCE_CONSUMER_KEY,
|
| 63 |
+
'client_secret': SALESFORCE_CONSUMER_SECRET,
|
| 64 |
+
'redirect_uri': SALESFORCE_CALLBACK_URL #important for refresh
|
| 65 |
+
}
|
| 66 |
+
response = requests.post(SALESFORCE_TOKEN_URL, data=token_data)
|
| 67 |
+
if response.status_code == 200:
|
| 68 |
+
token_response = response.json()
|
| 69 |
+
return token_response.get('access_token'), token_response.get('refresh_token'), token_response.get('instance_url')
|
| 70 |
+
else:
|
| 71 |
+
print(f"Error refreshing access token: {response.text}") # Log the error
|
| 72 |
+
return None, None, None
|
| 73 |
+
|
| 74 |
+
def query_salesforce(soql_query, access_token, instance_url):
|
| 75 |
+
"""
|
| 76 |
+
Executes a SOQL query against Salesforce.
|
| 77 |
+
Args:
|
| 78 |
+
soql_query: The SOQL query string.
|
| 79 |
+
access_token: The Salesforce access token.
|
| 80 |
+
instance_url: The Salesforce instance URL.
|
| 81 |
+
Returns:
|
| 82 |
+
A list of records from Salesforce, or None on error.
|
| 83 |
+
"""
|
| 84 |
+
headers = {'Authorization': f'Bearer {access_token}'}
|
| 85 |
+
url = f"{instance_url}/services/data/v59.0/query/?q={soql_query}" # Use API version v59.0
|
| 86 |
+
response = requests.get(url, headers=headers)
|
| 87 |
+
if response.status_code == 200:
|
| 88 |
+
return response.json().get('records')
|
| 89 |
+
else:
|
| 90 |
+
print(f"Error querying Salesforce: {response.text}") # Log the error
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
def send_data_to_salesforce(sobject_name, data, access_token, instance_url):
|
| 94 |
+
"""
|
| 95 |
+
Sends data to Salesforce to create a new record.
|
| 96 |
+
Args:
|
| 97 |
+
sobject_name: The Salesforce object name (e.g., 'Subcontractor_Performance_Score__c').
|
| 98 |
+
data: A dictionary containing the data to send.
|
| 99 |
+
access_token: The Salesforce access token.
|
| 100 |
+
instance_url: The Salesforce instance URL.
|
| 101 |
+
Returns:
|
| 102 |
+
The ID of the created record, or None on error.
|
| 103 |
+
"""
|
| 104 |
+
headers = {
|
| 105 |
+
'Authorization': f'Bearer {access_token}',
|
| 106 |
+
'Content-Type': 'application/json'
|
| 107 |
+
}
|
| 108 |
+
url = f"{instance_url}/services/data/v59.0/sobjects/{sobject_name}/" # Use API version v59.0
|
| 109 |
+
response = requests.post(url, headers=headers, json=data)
|
| 110 |
+
if 200 <= response.status_code < 300: # Successful creation (201 Created)
|
| 111 |
+
return response.json().get('id')
|
| 112 |
+
else:
|
| 113 |
+
print(f"Error sending data to Salesforce ({sobject_name}): {response.text}") # Log error
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# --- Flask Routes ---
|
| 119 |
+
|
| 120 |
+
@app.route('/')
|
| 121 |
+
def index():
|
| 122 |
+
"""
|
| 123 |
+
Displays a welcome message and instructions.
|
| 124 |
+
"""
|
| 125 |
+
return ("Welcome to the Subcontractor Performance Scorer API!\n"
|
| 126 |
+
"1. To authorize with Salesforce, visit /authorize_salesforce.\n"
|
| 127 |
+
"2. After authorization, vendor logs will be processed automatically (in a real implementation).\n"
|
| 128 |
+
"3. Scores will be sent to Salesforce (in a real implementation).\n")
|
| 129 |
+
|
| 130 |
+
@app.route('/authorize_salesforce')
|
| 131 |
+
def authorize_salesforce():
|
| 132 |
+
"""
|
| 133 |
+
Redirects the user to the Salesforce authorization URL to obtain an authorization code.
|
| 134 |
+
"""
|
| 135 |
+
authorization_url = (
|
| 136 |
+
f"{SALESFORCE_AUTH_URL}?"
|
| 137 |
+
"response_type=code&"
|
| 138 |
+
f"client_id={SALESFORCE_CONSUMER_KEY}&"
|
| 139 |
+
f"redirect_uri={SALESFORCE_CALLBACK_URL}&"
|
| 140 |
+
"scope=api" # Add other scopes as needed (e.g., 'refresh_token')
|
| 141 |
+
)
|
| 142 |
+
return jsonify({"authorization_url": authorization_url}) # Return as JSON for easier handling
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@app.route('/callback')
|
| 146 |
+
def callback():
|
| 147 |
+
"""
|
| 148 |
+
Handles the Salesforce callback after authorization. Exchanges the authorization code
|
| 149 |
+
for an access token and refresh token.
|
| 150 |
+
"""
|
| 151 |
+
global auth_code, access_token, refresh_token, SALESFORCE_INSTANCE_URL
|
| 152 |
+
code = request.args.get('code')
|
| 153 |
+
if not code:
|
| 154 |
+
return jsonify({'error': 'Authorization code not provided.'}), 400
|
| 155 |
+
auth_code = code # Store the auth code
|
| 156 |
+
access_token, refresh_token, SALESFORCE_INSTANCE_URL = get_salesforce_access_token(code)
|
| 157 |
+
if access_token:
|
| 158 |
+
return jsonify({'message': 'Successfully authorized with Salesforce! Access token and refresh token obtained.'}), 200
|
| 159 |
+
else:
|
| 160 |
+
return jsonify({'error': 'Failed to obtain access token.'}), 500
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@app.route('/process_logs') # You might trigger this from a Salesforce outbound message or scheduled job.
|
| 165 |
+
def process_logs():
|
| 166 |
+
"""
|
| 167 |
+
Retrieves vendor logs from Salesforce, processes them using the scoring model,
|
| 168 |
+
and sends the results to Salesforce.
|
| 169 |
+
"""
|
| 170 |
+
global access_token, refresh_token, SALESFORCE_INSTANCE_URL
|
| 171 |
+
if not access_token:
|
| 172 |
+
if auth_code:
|
| 173 |
+
access_token, refresh_token, SALESFORCE_INSTANCE_URL = get_salesforce_access_token(auth_code)
|
| 174 |
+
else:
|
| 175 |
+
return jsonify({'error': 'Not authorized with Salesforce. Please visit /authorize_salesforce first.'}), 401
|
| 176 |
+
|
| 177 |
+
if not access_token:
|
| 178 |
+
return jsonify({'error': 'Not authorized with Salesforce. Please visit /authorize_salesforce first.'}), 401
|
| 179 |
+
|
| 180 |
+
if not SALESFORCE_INSTANCE_URL:
|
| 181 |
+
return jsonify({'error': 'Salesforce instance URL is missing.'}), 500
|
| 182 |
+
|
| 183 |
+
try:
|
| 184 |
+
# 1. Fetch vendor logs from Salesforce (replace with your actual SOQL query)
|
| 185 |
+
soql_query = "SELECT Id, Vendor__c, Work_Completion_Details__c, Delay_Reports__c, Incident_Logs__c, Log_Date__c FROM Vendor_Log__c"
|
| 186 |
+
vendor_logs = query_salesforce(soql_query, access_token, SALESFORCE_INSTANCE_URL)
|
| 187 |
+
if not vendor_logs:
|
| 188 |
+
return jsonify({'error': 'Failed to retrieve vendor logs from Salesforce or no logs found.'}), 500
|
| 189 |
+
|
| 190 |
+
# 2. Process the vendor logs using the model
|
| 191 |
+
scores_for_salesforce = process_vendor_logs(vendor_logs) # Call your model.py function
|
| 192 |
+
|
| 193 |
+
if not scores_for_salesforce:
|
| 194 |
+
return jsonify({'warning': 'No scores to send to Salesforce.'}), 200 # No error, but no data sent
|
| 195 |
+
|
| 196 |
+
# 3. Send the scores to Salesforce
|
| 197 |
+
all_updates_successful = True
|
| 198 |
+
for score_data in scores_for_salesforce:
|
| 199 |
+
# Map the score_data to the fields in your Subcontractor_Performance_Score__c object
|
| 200 |
+
salesforce_data = {
|
| 201 |
+
'Vendor__c': score_data['vendor_id'], # You might need to look up the Salesforce ID
|
| 202 |
+
'Month_c': score_data['month'],
|
| 203 |
+
'Quality_Score__c': score_data['quality'],
|
| 204 |
+
'Timeliness_Score__c': score_data['timeliness'],
|
| 205 |
+
'Safety_Score__c': score_data['safety'],
|
| 206 |
+
'Communication_Score__c': score_data['communication'],
|
| 207 |
+
'Final_Score_c': score_data['final_score'],
|
| 208 |
+
'Alert_Flag_c': score_data['alert_flag'],
|
| 209 |
+
'Certification_URL_c': score_data['certificate_url'], #add this
|
| 210 |
+
'Trend_Deviation__c': score_data['trend_deviation']
|
| 211 |
+
}
|
| 212 |
+
record_id = send_data_to_salesforce('Subcontractor_Performance_Score__c', salesforce_data, access_token, SALESFORCE_INSTANCE_URL)
|
| 213 |
+
if not record_id:
|
| 214 |
+
all_updates_successful = False
|
| 215 |
+
print(f"Failed to send score for vendor {score_data['vendor_id']} to Salesforce.")
|
| 216 |
+
|
| 217 |
+
if all_updates_successful:
|
| 218 |
+
return jsonify({'message': 'Successfully processed vendor logs and sent scores to Salesforce.'}), 200
|
| 219 |
+
else:
|
| 220 |
+
return jsonify({'error': 'Failed to send some scores to Salesforce. Check logs for details.'}), 500
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
return jsonify({'error': f'An error occurred: {e}'}), 500
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@app.route('/refresh_token')
|
| 228 |
+
def refresh_token_route():
|
| 229 |
+
"""
|
| 230 |
+
Endpoint to refresh the Salesforce access token. This would be called periodically
|
| 231 |
+
(e.g., by a scheduled task) in a real application.
|
| 232 |
+
"""
|
| 233 |
+
global access_token, refresh_token, SALESFORCE_INSTANCE_URL
|
| 234 |
+
if not refresh_token:
|
| 235 |
+
return jsonify({'error': 'No refresh token available.'}), 400
|
| 236 |
+
|
| 237 |
+
new_access_token, new_refresh_token, SALESFORCE_INSTANCE_URL = refresh_salesforce_access_token(refresh_token)
|
| 238 |
+
if new_access_token:
|
| 239 |
+
access_token = new_access_token
|
| 240 |
+
refresh_token = new_refresh_token # Update the refresh token, as it might change.
|
| 241 |
+
return jsonify({'message': 'Access token refreshed successfully.'}), 200
|
| 242 |
+
else:
|
| 243 |
+
return jsonify({'error': 'Failed to refresh access token.'}), 500
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
if __name__ == '__main__':
|
| 247 |
+
# The port is automatically configured by Hugging Face Spaces
|
| 248 |
+
port = int(os.environ.get("PORT", 8080))
|
| 249 |
+
app.run(host='0.0.0.0', port=port, debug=True)
|