Загрузка данных


import builtins
import json
import os
import sys
import logging
import traceback
import requests
import uuid
from datetime import datetime, timedelta
from calendar import monthrange
from argparse import ArgumentParser
from io import StringIO
import csv
import time
import gc
import ctypes
from multiprocessing import Process, Queue

from umdap_config import LOG_FOLDER, ENV_FOLDER, EROUTER_HOST, PRIMARY_MADAB, UMDAP_ETL_URL_LIST

APPLICATION_NAME = 'externalGTT'
LOG_FILE = os.path.join(LOG_FOLDER, "externalGTT.log")
CONFIG_FILE = os.path.join(ENV_FOLDER, "externalGtt.json")
CREDENTIALS_FILE = os.path.join(ENV_FOLDER, "config.json")

BASE_URL = 'https://www.globaltradetracker.com/api/rest'
builtins.BASE_URL = BASE_URL
builtins.XML_DATE_FMT = '%d.%m.%Y %H:%M:%S'
builtins.DELIMITER = ','

session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'Connection': 'keep-alive',
})

last_update_file = os.path.join(ENV_FOLDER, "last_update.json")

BATCH_SIZE = 100000
batch_file = os.path.join(ENV_FOLDER, "current_batch.csv")
total_sent = 0
current_batch_count = 0

ALL_KEYS = [
    'id',
    'Reporter_Code', 'Trade_Flow', 'Is_Mirror_Data', 'Period',
    'Reporter_Name', 'Reporter_Description', 'Reporter_Source',
    'Incoterm', 'Partner_Code', 'Partner_Name',
    'Commodity_HS_Code', 'Commodity_Description',
    'HS6_Code', 'HS6_Code_Description',
    'Subdivision', 'Port', 'Transport', 'Foreign_Port', 'US_State',
    'Customs_Regime', 'Suppression',
    'Monetary_Value', 'Currency',
    'Secondary_Monetary_Value', 'Secondary_Currency',
    'Primary_Quantity', 'Primary_Quantity_Unit',
    'Secondary_Quantity', 'Secondary_Quantity_Unit',
    'Primary_Quantity_Price', 'Primary_Quantity_Price_Unit',
    'Secondary_Quantity_Price', 'Secondary_Quantity_Price_Unit'
]


def log(msg=''):
    """Логирование с временной меткой"""
    str_with_time = f'{datetime.now().strftime("%Y/%m/%d %H:%M:%S")} {msg}'
    print(str_with_time)
    with open(LOG_FILE, "a", encoding="utf-8") as file:
        file.write(f'{str_with_time}\n')

builtins.log = log


def generate_id():
    """Генерация 32-символьного ID в нижнем регистре"""
    return uuid.uuid4().hex


def get_token():
    """Получение токена для API"""
    try:
        if not os.path.exists(CREDENTIALS_FILE):
            log(f"ERROR: credentials file not found: {CREDENTIALS_FILE}")
            return None

        with open(CREDENTIALS_FILE, 'r') as f:
            credentials = json.load(f)

        link = f"{BASE_URL}/gettoken?userid={credentials['user_id']}&password={credentials['password']}"
        response = session.get(link, timeout=30)

        if response.status_code == 200:
            token = response.content.decode().strip()
            if token:
                log("Token received successfully")
                return token
        return None
    except Exception as e:
        log(f"ERROR in get_token: {e}")
        return None


def get_subscription_countries(token):
    """Получение списка стран"""
    try:
        response = session.get(f"{BASE_URL}/countries?token={token}", timeout=30)
        if response.status_code == 200:
            data = response.json()
            countries = []
            if isinstance(data, list):
                for item in data:
                    if isinstance(item, dict):
                        country_data = item.get('country', item)
                        if country_data.get('reportingcountry') == 'true' and country_data.get('defaultReporter') == True:
                            code = country_data.get('reportercode', '')
                            if code:
                                countries.append(code)
            countries = list(dict.fromkeys(countries))
            log(f"Available default reporters ({len(countries)})")
            return countries
        return []
    except Exception as e:
        log(f"ERROR in get_subscription_countries: {e}")
        return []


def get_last_update_time():
    """Чтение времени последнего обновления"""
    try:
        if os.path.exists(last_update_file):
            with open(last_update_file, 'r') as f:
                data = json.load(f)
                return data.get('last_update', None)
        return None
    except Exception as e:
        log(f"ERROR in get_last_update_time: {e}")
        return None


def save_last_update_time(update_time):
    """Сохранение времени последнего обновления"""
    try:
        with open(last_update_file, 'w') as f:
            json.dump({'last_update': update_time}, f)
    except Exception as e:
        log(f"ERROR in save_last_update_time: {e}")


def parse_period(year, month):
    """Парсинг периода"""
    try:
        year = int(year)
        month = int(month)
        last_day = monthrange(year, month)[1]
        return datetime(year, month, last_day).strftime('%Y-%m-%d')
    except:
        return '2000-01-01'


def parse_record_to_dict(record, reporter_code="", trade_flow=""):
    """Парсинг записи из нового формата API"""
    try:
        if not isinstance(record, dict):
            return None

        year = record.get('year', 2000)
        month = record.get('month', 1)

        result = {
            'id': generate_id(),
            'Reporter_Code': record.get('reporterCode', reporter_code),
            'Trade_Flow': record.get('tradeFlow', trade_flow),
            'Is_Mirror_Data': record.get('isMirror', ''),
            'Period': parse_period(year, month),
            'Reporter_Name': record.get('reporterName', ''),
            'Reporter_Description': record.get('reporterDescription', '')[:120],
            'Reporter_Source': record.get('reporterSource', '')[:120],
            'Incoterm': record.get('incoterm', '')[:60],
            'Partner_Code': record.get('partnerCode', '')[:60],
            'Partner_Name': record.get('partnerName', '')[:120],
            'Commodity_HS_Code': record.get('hsCode', '')[:60],
            'Commodity_Description': record.get('commodityDescriptionOriginal', '')[:250],
            'HS6_Code': record.get('hs6Code', '')[:60],
            'HS6_Code_Description': record.get('hs6DescriptionOriginal', '')[:250],
            'Subdivision': record.get('subdivision', '')[:120],
            'Port': record.get('port', '')[:120],
            'Transport': record.get('transport', '')[:120],
            'Foreign_Port': record.get('foreignPort', '')[:120],
            'US_State': record.get('usState', '')[:120],
            'Customs_Regime': record.get('customsRegime', '')[:120],
            'Suppression': record.get('suppression', '')[:3900],
            'Monetary_Value': record.get('value', 0),
            'Currency': record.get('currency', 'USD')[:60],
            'Secondary_Monetary_Value': record.get('value2', 0),
            'Secondary_Currency': record.get('currency2', '')[:60],
            'Primary_Quantity': record.get('quantity1', 0),
            'Primary_Quantity_Unit': record.get('unit1', '')[:60],
            'Secondary_Quantity': record.get('quantity2', 0),
            'Secondary_Quantity_Unit': record.get('unit2', '')[:60],
            'Primary_Quantity_Price': record.get('price1', 0),
            'Primary_Quantity_Price_Unit': record.get('priceunit1', '')[:60],
            'Secondary_Quantity_Price': record.get('price2', 0),
            'Secondary_Quantity_Price_Unit': record.get('priceunit2', '')[:60],
        }

        # ВРЕМЕННЫЙ ЛОГ — только первые 3 записи
        global current_batch_count
        if current_batch_count < 3:
            log(f"DEBUG: id={result['id']}, reporter={result['Reporter_Code']}, period={result['Period']}")

        return result
    except Exception as e:
        log(f"ERROR in parse_record_to_dict: {e}")
        return None


def create_batch_file():
    """Создаёт новый CSV файл с заголовком"""
    if os.path.exists(batch_file):
        os.remove(batch_file)

    with open(batch_file, 'w', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=ALL_KEYS)
        writer.writeheader()


def write_to_batch(record):
    """Записывает одну запись в файл"""
    global current_batch_count

    with open(batch_file, 'a', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=ALL_KEYS)
        writer.writerow(record)

    current_batch_count += 1


def send_file_in_subprocess(file_path, queue):
    """Отправка файла в отдельном процессе"""
    try:
        from etl_common import process_etls

        def batch_request(url):
            with open(file_path, 'r') as f:
                return f.read()

        process_etls(
            application_name=APPLICATION_NAME,
            etl_desc_url=f'{EROUTER_HOST}/procedure/{PRIMARY_MADAB}/umdap.etl.description/call.json?application={APPLICATION_NAME}',
            data_type=None,
            api_requestor=batch_request,
            erouter_url_list=UMDAP_ETL_URL_LIST
        )

        queue.put(('success', 'ok'))
    except Exception as e:
        queue.put(('error', str(e)))


def send_batch_file():
    """Отправляет текущий файл в DWH"""
    global total_sent, current_batch_count

    if current_batch_count == 0:
        return

    log(f"Sending batch of {current_batch_count} records...")

    # ВРЕМЕННЫЙ ЛОГ — проверить файл перед отправкой
    try:
        with open(batch_file, 'r') as f:
            first_lines = [f.readline() for _ in range(3)]
        log(f"DEBUG CSV head: {first_lines}")
    except Exception as e:
        log(f"ERROR reading CSV: {e}")

    queue = Queue()
    p = Process(target=send_file_in_subprocess, args=(batch_file, queue))
    p.start()
    p.join(timeout=600)

    if p.is_alive():
        log("✗ Timeout! Terminating...")
        p.terminate()
        p.join()
    else:
        if not queue.empty():
            status, result = queue.get()
            if status == 'success':
                total_sent += current_batch_count
                log(f"✓ Batch sent. Total sent: {total_sent}")
            else:
                log(f"✗ Error: {result}")

    current_batch_count = 0
    create_batch_file()
    gc.collect()

    try:
        libc = ctypes.CDLL("libc.so.6")
        libc.malloc_trim(0)
        log("Memory released to OS")
    except:
        pass


def add_record(record, reporter_code="", trade_flow=""):
    """Обрабатывает одну запись"""
    parsed = parse_record_to_dict(record, reporter_code, trade_flow)
    if parsed:
        write_to_batch(parsed)

        if current_batch_count >= BATCH_SIZE:
            send_batch_file()


def process_records(records, reporter_code="", trade_flow=""):
    """Обрабатывает список записей"""
    for record in records:
        if isinstance(record, dict):
            add_record(record, reporter_code, trade_flow)


def get_historical_data(token, countries):
    """БЛОК 1: Исторический сбор с 2013 по 2026"""
    log("=" * 60)
    log("BLOCK 1: Starting historical data collection from 2013")
    log(f"Countries ({len(countries)}), HS Codes: 31,25")
    log("=" * 60)

    create_batch_file()

    total_requests = 0
    hs_filter = '31,25'

    fields = 'reporterCode,tradeFlow,isMirror,year,month,reporterName,reporterDescription,reporterSource,incoterm,partnerCode,partnerName,hsCode,commodityDescriptionOriginal,hs6Code,hs6DescriptionOriginal,value,currency,value2,currency2,quantity1,unit1,quantity2,unit2,price1,priceunit1,price2,priceunit2,port,transport,subdivision'

    trade_details = 'PORT,SUBDIVISION,TRANSPORT,CUSTOMS_REGIME,FOREIGN_PORT,FOREIGN_SUBDIVISION'

    for country in countries:
        for impexp in ['E', 'I']:
            try:
                params = {
                    'token': token,
                    'hscode': hs_filter,
                    'reporter': country,
                    'impexp': impexp,
                    'from': '2013-01',
                    'to': '2026-08',
                    'layout': 'brief',
                    'hslevel': '6',
                    'field': fields,
                    'tradedetails': trade_details,
                    'currency2': 'EUR',
                    'decimalscale': '2',
                    'format': 'json'
                }

                total_requests += 1

                start_time = time.time()
                response = session.get(f"{BASE_URL}/getreport", params=params, timeout=600)
                end_time = time.time()

                log(f"[{total_requests}] {country} HS={hs_filter} {impexp} Status={response.status_code} Time={end_time-start_time:.1f}s Batch={current_batch_count} Sent={total_sent}")

                if response.status_code == 200:
                    data = response.json()

                    if isinstance(data, list) and len(data) > 0:
                        process_records(data, country, impexp)
                    elif isinstance(data, dict):
                        if 'reportData' in data:
                            process_records(data['reportData'], country, impexp)
                        elif 'data' in data:
                            process_records(data['data'], country, impexp)

            except Exception as e:
                log(f"✗ {str(e)}")
                continue

    if current_batch_count > 0:
        send_batch_file()

    if os.path.exists(batch_file):
        os.remove(batch_file)

    log(f"\nCOMPLETED: {total_requests} requests, {total_sent} records sent")


def get_updated_data(token, countries):
    """БЛОК 2: Регулярное обновление"""
    log("=" * 60)
    log("BLOCK 2: Checking for updates")
    log("=" * 60)

    last_update = get_last_update_time()
    current_date = datetime.now().strftime('%Y-%m')

    create_batch_file()

    hs_filter = '31,25'

    fields = 'reporterCode,tradeFlow,isMirror,year,month,reporterName,reporterDescription,reporterSource,incoterm,partnerCode,partnerName,hsCode,commodityDescriptionOriginal,hs6Code,hs6DescriptionOriginal,value,currency,value2,currency2,quantity1,unit1,quantity2,unit2,price1,priceunit1,price2,priceunit2,port,transport,subdivision'

    trade_details = 'PORT,SUBDIVISION,TRANSPORT,CUSTOMS_REGIME,FOREIGN_PORT,FOREIGN_SUBDIVISION'

    for country in countries:
        for impexp in ['E', 'I']:
            try:
                params = {
                    'token': token,
                    'hscode': hs_filter,
                    'reporter': country,
                    'impexp': impexp,
                    'updatedAfter': last_update if last_update else '2013-01',
                    'latestavailablemonths': 3,
                    'layout': 'brief',
                    'hslevel': '6',
                    'field': fields,
                    'tradedetails': trade_details,
                    'currency2': 'EUR',
                    'decimalscale': '2',
                    'format': 'json'
                }

                response = session.get(f"{BASE_URL}/getreport", params=params, timeout=300)
                if response.status_code == 200:
                    data = response.json()
                    if isinstance(data, list) and len(data) > 0:
                        process_records(data, country, impexp)
                    elif isinstance(data, dict) and 'reportData' in data:
                        process_records(data['reportData'], country, impexp)

            except Exception as e:
                log(f"✗ {str(e)}")
                continue

    if current_batch_count > 0:
        send_batch_file()

    if os.path.exists(batch_file):
        os.remove(batch_file)

    save_last_update_time(current_date)

    log(f"Updated. Total sent: {total_sent}")


def main():
    """Основная функция"""
    log('--------------------- Script started ------------------------------')

    try:
        token = get_token()
        if not token:
            log("ERROR: Failed to get token")
            sys.exit(1)

        countries = get_subscription_countries(token)

        if not countries:
            log("ERROR: Failed to get parameters")
            sys.exit(1)

        last_update = get_last_update_time()

        if last_update is None:
            log("First run - BLOCK 1: Historical collection")
            get_historical_data(token, countries)
            save_last_update_time(datetime.now().strftime('%Y-%m'))
        else:
            log(f"Regular run - BLOCK 2: Update (last: {last_update})")
            get_updated_data(token, countries)

        log(f"Total sent: {total_sent}")

    except Exception as e:
        log(f'Error: {traceback.format_exception(*sys.exc_info())}')
        sys.exit(1)
    finally:
        log('--------------------- Script finished -----------------------------')


if __name__ == "__main__":
    main()