Add error handling and retry logic to API calls

- Add retry decorator for transient errors in dinect_api.py
- Add error handling decorator for consistent error responses
- Update bonuses_update to handle numeric bonus amounts properly
- Fix parameter name typo in bonuses_update
- Skip invalid phone numbers and rows in app.py
- Add duplicate doc_id detection in transaction processing
- Improve logging of successful transactions
- Remove redundant doc_id generation code
- Fix file closing logic in app.py
This commit is contained in:
Sergey_Z
2026-02-26 17:34:43 +03:00
parent b8e9b16bd9
commit 2df74566aa
2 changed files with 151 additions and 12 deletions

37
app.py
View File

@@ -104,6 +104,7 @@ def run_import():
if phone == '': if phone == '':
print(f'error in line: [{line_count}]- Invalid phone number: {phone}') print(f'error in line: [{line_count}]- Invalid phone number: {phone}')
log_file.write(f'error in line: [{line_count}]- Invalid phone number: {phone}\n') log_file.write(f'error in line: [{line_count}]- Invalid phone number: {phone}\n')
continue # Skip invalid phone
# validate email # validate email
email = email.strip() email = email.strip()
@@ -131,6 +132,7 @@ def run_import():
except ValueError as e: except ValueError as e:
ret = f'Unexpected error in line: [{line_count}] {repr(e)}' ret = f'Unexpected error in line: [{line_count}] {repr(e)}'
log_file.write(f'{ret}\n') log_file.write(f'{ret}\n')
continue # Skip invalid rows
# Find user via the API # Find user via the API
# by card # by card
@@ -176,6 +178,7 @@ def run_import():
print(f'Processing "transaction" file: {f}') print(f'Processing "transaction" file: {f}')
csv_reader = csv.reader(csv_file, delimiter=',') csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0 line_count = 0
processed_doc_ids = set() # Track processed doc_ids to detect duplicates
for row in csv_reader: for row in csv_reader:
if line_count == 0: if line_count == 0:
line_count += 1 line_count += 1
@@ -185,6 +188,30 @@ def run_import():
print(f'Processing line {line_count}: {row}') print(f'Processing line {line_count}: {row}')
user_id, card, phone, summ_total, summ_discount, sum_with_discount, bonus_amount, transaction_date, transaction_time, doc_id = row user_id, card, phone, summ_total, summ_discount, sum_with_discount, bonus_amount, transaction_date, transaction_time, doc_id = row
# Validate numeric fields
try:
float(summ_total) if summ_total.strip() else 0.0
float(summ_discount) if summ_discount.strip() else 0.0
float(sum_with_discount) if sum_with_discount.strip() else 0.0
float(bonus_amount) if bonus_amount.strip() else 0.0
except ValueError as ve:
print(f'error in line: [{line_count}]- Invalid numeric value: {ve}')
log_file.write(f'error in line: [{line_count}]- Invalid numeric value: {ve}\n')
continue
# Check for duplicate doc_id
if doc_id.strip() == '':
doc_id = f'{file_name[0:20]}-{line_count}-{card}'
else:
doc_id = doc_id.strip()
# Check for duplicate doc_id in current run
if doc_id in processed_doc_ids:
print(f'error in line: [{line_count}]- Duplicate doc_id: {doc_id}')
log_file.write(f'error in line: [{line_count}]- Duplicate doc_id: {doc_id}\n')
continue
processed_doc_ids.add(doc_id)
if card.strip() != '': if card.strip() != '':
user_found_by_card, din_id, _, _, _ = get_user(card, get_type='auto') user_found_by_card, din_id, _, _, _ = get_user(card, get_type='auto')
if not user_found_by_card: if not user_found_by_card:
@@ -210,11 +237,6 @@ def run_import():
user_found = user_found_by_card or user_found_by_phone user_found = user_found_by_card or user_found_by_phone
if user_found: if user_found:
if doc_id.strip() == '':
doc_id = f'{file_name[0:20]}-{line_count}-{card}'
else:
doc_id = doc_id.strip()
result, data = bonuses_update( result, data = bonuses_update(
user_id=din_id, user_id=din_id,
summ_total=summ_total, summ_total=summ_total,
@@ -230,6 +252,7 @@ def run_import():
log_file.write(f'error in line: [{line_count}]- bonuses_update: {data}\n') log_file.write(f'error in line: [{line_count}]- bonuses_update: {data}\n')
else: else:
print(f'RESULT=OK, Bonuses updated, user_id={din_id}') print(f'RESULT=OK, Bonuses updated, user_id={din_id}')
log_file.write(f'Line: [{line_count}]- Success: doc_id={doc_id}, bonus={bonus_amount}\n')
else: else:
print(f'error in line: [{line_count}]- Invalid user: {user_id}') print(f'error in line: [{line_count}]- Invalid user: {user_id}')
@@ -238,13 +261,11 @@ def run_import():
except ValueError as e: except ValueError as e:
ret = f'error in line: [{line_count}] {repr(e)}' ret = f'error in line: [{line_count}] {repr(e)}'
log_file.write(f'{ret}\n') log_file.write(f'{ret}\n')
continue
end_time = time.time() end_time = time.time()
print(f'Elapsed time of TRANSACTIONS file processing : {end_time - start_time} seconds') print(f'Elapsed time of TRANSACTIONS file processing : {end_time - start_time} seconds')
csv_file.close()
log_file.close()
os.rename(f, f + '.' + time.strftime("%Y%m%d-%H%M%S") + '.old') os.rename(f, f + '.' + time.strftime("%Y%m%d-%H%M%S") + '.old')

View File

@@ -1,14 +1,66 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# __author__ = 'szhdanoff@gmail.com' # __author__ = 'szhdanoff@gmail.com'
__version__ = '1.0.3' __version__ = '1.0.4'
import os import os
import requests import requests
import json import json
import time
from functools import wraps
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
def retry_on_error(max_retries=3, delay=1, backoff=2):
"""
Decorator for retrying API calls on transient errors.
Args:
max_retries: Maximum number of retry attempts
delay: Initial delay between retries in seconds
backoff: Multiplier for delay after each retry
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
current_delay = delay
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except (requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
requests.exceptions.HTTPError) as e:
last_exception = e
if attempt < max_retries:
print(f"Retry {attempt + 1}/{max_retries} after {current_delay}s - Error: {e}")
time.sleep(current_delay)
current_delay *= backoff
else:
print(f"All {max_retries} retries exhausted. Last error: {e}")
raise last_exception
return wrapper
return decorator
def handle_api_error(func):
"""
Decorator for handling API errors consistently.
Returns tuple (False, error_message) on any exception.
"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except requests.exceptions.RequestException as e:
return False, f"Network error: {str(e)}"
except (ValueError, KeyError) as e:
return False, f"Data error: {str(e)}"
except Exception as e:
return False, f"Unexpected error: {str(e)}"
return wrapper
is_prod = bool(os.getenv('PRODUCTION', False)) is_prod = bool(os.getenv('PRODUCTION', False))
if is_prod: if is_prod:
url = 'https://pos-api.dinect.com/20130701/' url = 'https://pos-api.dinect.com/20130701/'
@@ -51,6 +103,12 @@ def get_user(search_id, get_type='auto', headers=None) -> tuple:
- The boolean value indicating success or failure. - The boolean value indicating success or failure.
- The response data based on the request made. - The response data based on the request made.
""" """
return _get_user_impl(search_id, get_type, headers)
@retry_on_error(max_retries=3, delay=1, backoff=2)
@handle_api_error
def _get_user_impl(search_id, get_type='auto', headers=None) -> tuple:
if headers is None: if headers is None:
headers = HEADERS headers = HEADERS
@@ -129,6 +187,12 @@ def new_user_by_card(external_card, full_name, phone, gender=1, email=None, head
If the request is successful, the boolean is True and the JSON response is returned. If the request is successful, the boolean is True and the JSON response is returned.
If the request is unsuccessful, the boolean is False and the error message is returned. If the request is unsuccessful, the boolean is False and the error message is returned.
""" """
return _new_user_by_card_impl(external_card, full_name, phone, gender, email, headers, use_existing)
@retry_on_error(max_retries=3, delay=1, backoff=2)
@handle_api_error
def _new_user_by_card_impl(external_card, full_name, phone, gender=1, email=None, headers=None, use_existing=True):
if headers is None: if headers is None:
headers = HEADERS headers = HEADERS
@@ -149,6 +213,26 @@ def new_user_by_card(external_card, full_name, phone, gender=1, email=None, head
return False, r.text return False, r.text
if headers is None:
headers = HEADERS
base_url = url + '/users/'
params = {
# "short_name": nickname,
"full_name": full_name,
"phone": phone,
"email": email,
"gender": gender,
}
r = requests.post(base_url, headers=headers, json=params)
if r.status_code == 201:
return True, r.json()
else:
return False, r.json()
def add_external_card(user_id, card, headers=None): def add_external_card(user_id, card, headers=None):
""" """
Adds an external card to a user. Adds an external card to a user.
@@ -173,6 +257,8 @@ def add_external_card(user_id, card, headers=None):
r = requests.post(base_url, headers=headers, json=params) r = requests.post(base_url, headers=headers, json=params)
if r.status_code == 201: if r.status_code == 201:
return True, r.json() return True, r.json()
else:
return False, r.text if r.text else f"HTTP {r.status_code}"
def bonuses_update( def bonuses_update(
@@ -184,6 +270,37 @@ def bonuses_update(
doc_id, doc_id,
headers=None, headers=None,
dry_run=False dry_run=False
):
"""
Updates user bonuses (adds or subtracts bonus points).
Args:
user_id: User ID in Dinect system
summ_total: Total sum of transaction
bonus_amount: Bonus amount to add (positive) or pay with bonuses (negative)
sum_with_discount: Sum after discount
sum_discount: Discount amount
doc_id: Document ID for the transaction
headers: Optional request headers
dry_run: If True, simulates the transaction without actual processing
Returns:
tuple: (success: bool, response_data: dict|str)
"""
return _bonuses_update_impl(user_id, summ_total, bonus_amount, sum_with_discount, sum_discount, doc_id, headers, dry_run)
@retry_on_error(max_retries=3, delay=1, backoff=2)
@handle_api_error
def _bonuses_update_impl(
user_id,
summ_total,
bonus_amount,
sum_with_discount,
sum_discount,
doc_id,
headers=None,
dry_run=False
): ):
if headers is None: if headers is None:
headers = HEADERS headers = HEADERS
@@ -194,7 +311,7 @@ def bonuses_update(
# "bonus_amount": bonus_amount, # "bonus_amount": bonus_amount,
"sum_total": summ_total, "sum_total": summ_total,
"sum_discount": sum_discount, "sum_discount": sum_discount,
"sum_with_discount ": sum_with_discount, "sum_with_discount": sum_with_discount,
"commit": 'True', "commit": 'True',
"curr_iso_name": currency, "curr_iso_name": currency,
"override": 'True' "override": 'True'
@@ -202,9 +319,10 @@ def bonuses_update(
} }
bonus_amount_numeric = float(bonus_amount) bonus_amount_numeric = float(bonus_amount)
if bonus_amount_numeric >= 0: if bonus_amount_numeric >= 0:
params["bonus_amount"] = bonus_amount params["bonus_amount"] = str(int(bonus_amount_numeric))
else: else:
params["bonus_payment"] = bonus_amount[1:] # Negative value - pay with bonuses (use absolute value)
params["bonus_payment"] = str(int(abs(bonus_amount_numeric)))
r = requests.post(base_url, headers=headers, json=params) r = requests.post(base_url, headers=headers, json=params)
if r.status_code == 201: if r.status_code == 201: