3v324v23 commited on
Commit
98897be
·
1 Parent(s): a241a73

feat: 新增贸易查询筛选条件并完善巴西贸易数据解析

Browse files

- 扩展TradeQueryRequest查询schema,新增贸易方向、商品名称、原产国、目的国等筛选参数
- 更新贸易路由文档,补充新支持的筛选维度
- 为贸易路由添加对应新增参数的数据库筛选逻辑
- 重构巴西ComexStat连接器,新增官方表格数据加载与标准化映射工具
- 完善巴西贸易记录的国家、港口、运输方式及商品名称解析逻辑
- 补充完整的巴西运输方式标准化映射表

apps/api/routers/__pycache__/trade.cpython-314.pyc ADDED
Binary file (4.64 kB). View file
 
apps/api/routers/trade.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, Depends, Query, HTTPException
2
  from sqlalchemy.ext.asyncio import AsyncSession
3
  from sqlalchemy import select, func, desc
4
 
@@ -15,7 +15,7 @@ async def search_trade_records(
15
  ):
16
  """
17
  检索标准贸易记录
18
- 支持按国家、HS 编码进口模糊查询及日期范围筛选
19
  """
20
  stmt = select(StandardTradeRecord)
21
  count_stmt = select(func.count()).select_from(StandardTradeRecord)
@@ -24,10 +24,20 @@ async def search_trade_records(
24
  filters = []
25
  if query.source_country:
26
  filters.append(StandardTradeRecord.source_country == query.source_country)
 
 
27
  if query.hs_code:
28
  filters.append(StandardTradeRecord.hs_code.like(f"{query.hs_code}%"))
 
 
29
  if query.importer_name:
30
  filters.append(StandardTradeRecord.importer_name.ilike(f"%{query.importer_name}%"))
 
 
 
 
 
 
31
  if query.start_date:
32
  filters.append(StandardTradeRecord.trade_date >= query.start_date)
33
  if query.end_date:
 
1
+ from fastapi import APIRouter, Depends
2
  from sqlalchemy.ext.asyncio import AsyncSession
3
  from sqlalchemy import select, func, desc
4
 
 
15
  ):
16
  """
17
  检索标准贸易记录
18
+ 支持按国家、方向、HS、商品、企业名、起运/目的国及日期范围筛选
19
  """
20
  stmt = select(StandardTradeRecord)
21
  count_stmt = select(func.count()).select_from(StandardTradeRecord)
 
24
  filters = []
25
  if query.source_country:
26
  filters.append(StandardTradeRecord.source_country == query.source_country)
27
+ if query.trade_direction:
28
+ filters.append(StandardTradeRecord.trade_direction == query.trade_direction)
29
  if query.hs_code:
30
  filters.append(StandardTradeRecord.hs_code.like(f"{query.hs_code}%"))
31
+ if query.product_name:
32
+ filters.append(StandardTradeRecord.product_name.ilike(f"%{query.product_name}%"))
33
  if query.importer_name:
34
  filters.append(StandardTradeRecord.importer_name.ilike(f"%{query.importer_name}%"))
35
+ if query.exporter_name:
36
+ filters.append(StandardTradeRecord.exporter_name.ilike(f"%{query.exporter_name}%"))
37
+ if query.origin_country:
38
+ filters.append(StandardTradeRecord.origin_country == query.origin_country)
39
+ if query.destination_country:
40
+ filters.append(StandardTradeRecord.destination_country == query.destination_country)
41
  if query.start_date:
42
  filters.append(StandardTradeRecord.trade_date >= query.start_date)
43
  if query.end_date:
apps/api/schemas/__pycache__/trade.cpython-314.pyc ADDED
Binary file (4.7 kB). View file
 
apps/api/schemas/trade.py CHANGED
@@ -4,9 +4,13 @@ from datetime import datetime
4
 
5
  class TradeQueryRequest(BaseModel):
6
  source_country: Optional[str] = Field(None, description="来源国 (如 US, BR)")
 
7
  hs_code: Optional[str] = Field(None, description="HS编码 (模糊前缀匹配)")
 
8
  importer_name: Optional[str] = Field(None, description="进口商名称 (模糊匹配)")
9
  exporter_name: Optional[str] = Field(None, description="出口商名称 (模糊匹配)")
 
 
10
  start_date: Optional[datetime] = Field(None, description="起始时间")
11
  end_date: Optional[datetime] = Field(None, description="结束时间")
12
  page: int = Field(1, ge=1, description="页码")
 
4
 
5
  class TradeQueryRequest(BaseModel):
6
  source_country: Optional[str] = Field(None, description="来源国 (如 US, BR)")
7
+ trade_direction: Optional[str] = Field(None, description="贸易方向 (import / export)")
8
  hs_code: Optional[str] = Field(None, description="HS编码 (模糊前缀匹配)")
9
+ product_name: Optional[str] = Field(None, description="商品描述 (模糊匹配)")
10
  importer_name: Optional[str] = Field(None, description="进口商名称 (模糊匹配)")
11
  exporter_name: Optional[str] = Field(None, description="出口商名称 (模糊匹配)")
12
+ origin_country: Optional[str] = Field(None, description="原产国/启运国")
13
+ destination_country: Optional[str] = Field(None, description="目的国")
14
  start_date: Optional[datetime] = Field(None, description="起始时间")
15
  end_date: Optional[datetime] = Field(None, description="结束时间")
16
  page: int = Field(1, ge=1, description="页码")
logs/app_2026-06-10.log CHANGED
@@ -9,3 +9,39 @@
9
  2026-06-10 22:40:51.104 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
10
  2026-06-10 22:40:51.104 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
11
  2026-06-10 22:40:51.106 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv | Attempt: 2/3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  2026-06-10 22:40:51.104 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
10
  2026-06-10 22:40:51.104 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
11
  2026-06-10 22:40:51.106 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv | Attempt: 2/3
12
+ 2026-06-10 22:54:10.790 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv | Attempt: 1/3
13
+ 2026-06-10 22:54:12.527 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
14
+ 2026-06-10 22:54:12.536 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
15
+ 2026-06-10 22:54:12.555 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv | Attempt: 2/3
16
+ 2026-06-10 22:55:04.641 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv | Attempt: 1/3
17
+ 2026-06-10 22:55:07.338 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
18
+ 2026-06-10 22:55:07.341 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv
19
+ 2026-06-10 22:55:07.356 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2026.csv | Attempt: 2/3
20
+ 2026-06-10 22:55:37.527 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv | Attempt: 1/3
21
+ 2026-06-10 22:55:39.243 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv
22
+ 2026-06-10 22:55:39.247 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv
23
+ 2026-06-10 22:55:39.264 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv | Attempt: 2/3
24
+ 2026-06-10 22:55:42.802 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv | Attempt: 1/3
25
+ 2026-06-10 22:55:44.414 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv
26
+ 2026-06-10 22:55:44.416 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv
27
+ 2026-06-10 22:55:44.418 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv | Attempt: 2/3
28
+ 2026-06-10 22:55:47.150 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv | Attempt: 1/3
29
+ 2026-06-10 22:55:48.884 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv
30
+ 2026-06-10 22:55:48.886 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv
31
+ 2026-06-10 22:55:48.888 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv | Attempt: 2/3
32
+ 2026-06-10 22:55:53.270 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/NCM.csv | Attempt: 1/3
33
+ 2026-06-10 22:55:55.969 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/NCM.csv
34
+ 2026-06-10 22:55:55.979 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/NCM.csv
35
+ 2026-06-10 22:55:55.992 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/NCM.csv | Attempt: 2/3
36
+ 2026-06-10 22:56:54.255 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv | Attempt: 1/3
37
+ 2026-06-10 22:56:56.671 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv
38
+ 2026-06-10 22:56:56.678 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv
39
+ 2026-06-10 22:56:56.698 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/PAIS.csv | Attempt: 2/3
40
+ 2026-06-10 22:57:04.412 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv | Attempt: 1/3
41
+ 2026-06-10 22:57:06.215 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv
42
+ 2026-06-10 22:57:06.250 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv
43
+ 2026-06-10 22:57:06.306 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/URF.csv | Attempt: 2/3
44
+ 2026-06-10 22:57:11.485 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv | Attempt: 1/3
45
+ 2026-06-10 22:57:13.171 | WARNING | packages.core.http_client:request:79 - Request Error: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1081) for https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv
46
+ 2026-06-10 22:57:13.172 | WARNING | packages.core.http_client:request:82 - SSL 证书校验失败,降级为 verify=False 重试: https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv
47
+ 2026-06-10 22:57:13.174 | DEBUG | packages.core.http_client:request:55 - Request: GET https://balanca.economia.gov.br/balanca/bd/tabelas/VIA.csv | Attempt: 2/3
packages/connectors/__pycache__/brazil.cpython-314.pyc CHANGED
Binary files a/packages/connectors/__pycache__/brazil.cpython-314.pyc and b/packages/connectors/__pycache__/brazil.cpython-314.pyc differ
 
packages/connectors/brazil.py CHANGED
@@ -7,7 +7,12 @@ from typing import Any, Dict, List, Optional
7
  from packages.connectors.base import BaseConnector
8
  from packages.core.http_client import http_client
9
  from packages.core.models import RawTradeRecord, StandardTradeRecord
10
- from packages.dictionaries.brazil import TRANSPORT_MODE_MAP
 
 
 
 
 
11
 
12
  class BrazilComexStatConnector(BaseConnector):
13
  """
@@ -115,8 +120,10 @@ class BrazilComexStatConnector(BaseConnector):
115
  day=1,
116
  )
117
  ncm_code = (data.get("co_ncm") or "").strip()
118
- partner_country = self._truncate(data.get("co_pais"), 10)
119
- transport_mode = TRANSPORT_MODE_MAP.get((data.get("co_via") or "").strip().upper(), "UNKNOWN")
 
 
120
  record_id = str(uuid.uuid5(uuid.NAMESPACE_URL, f"{self.country_code}:{raw.id}"))
121
 
122
  return StandardTradeRecord(
@@ -129,15 +136,15 @@ class BrazilComexStatConnector(BaseConnector):
129
  importer_name=None,
130
  exporter_name=None,
131
  hs_code=ncm_code,
132
- product_name=f"NCM {ncm_code}" if ncm_code else None,
133
  amount=self._safe_float(data.get("vl_fob")),
134
  currency="USD",
135
  weight=self._safe_float(data.get("kg_liquido")),
136
  weight_unit="KG",
137
- origin_country=partner_country if trade_direction == "import" else self.country_code,
138
- destination_country=partner_country if trade_direction == "export" else self.country_code,
139
- departure_port=self._truncate(data.get("co_urf"), 100) if trade_direction == "export" else None,
140
- arrival_port=self._truncate(data.get("co_urf"), 100) if trade_direction == "import" else None,
141
  transport_mode=transport_mode,
142
  )
143
 
 
7
  from packages.connectors.base import BaseConnector
8
  from packages.core.http_client import http_client
9
  from packages.core.models import RawTradeRecord, StandardTradeRecord
10
+ from packages.dictionaries.brazil import (
11
+ get_country_alpha3,
12
+ get_ncm_name,
13
+ get_transport_mode,
14
+ get_urf_name,
15
+ )
16
 
17
  class BrazilComexStatConnector(BaseConnector):
18
  """
 
120
  day=1,
121
  )
122
  ncm_code = (data.get("co_ncm") or "").strip()
123
+ partner_country = await get_country_alpha3(data.get("co_pais"))
124
+ urf_name = await get_urf_name(data.get("co_urf"))
125
+ transport_mode = await get_transport_mode(data.get("co_via"))
126
+ product_name = await get_ncm_name(ncm_code)
127
  record_id = str(uuid.uuid5(uuid.NAMESPACE_URL, f"{self.country_code}:{raw.id}"))
128
 
129
  return StandardTradeRecord(
 
136
  importer_name=None,
137
  exporter_name=None,
138
  hs_code=ncm_code,
139
+ product_name=product_name or (f"NCM {ncm_code}" if ncm_code else None),
140
  amount=self._safe_float(data.get("vl_fob")),
141
  currency="USD",
142
  weight=self._safe_float(data.get("kg_liquido")),
143
  weight_unit="KG",
144
+ origin_country=partner_country if trade_direction == "import" else "BRA",
145
+ destination_country=partner_country if trade_direction == "export" else "BRA",
146
+ departure_port=self._truncate(urf_name or data.get("co_urf"), 100) if trade_direction == "export" else None,
147
+ arrival_port=self._truncate(urf_name or data.get("co_urf"), 100) if trade_direction == "import" else None,
148
  transport_mode=transport_mode,
149
  )
150
 
packages/dictionaries/__pycache__/brazil.cpython-314.pyc CHANGED
Binary files a/packages/dictionaries/__pycache__/brazil.cpython-314.pyc and b/packages/dictionaries/__pycache__/brazil.cpython-314.pyc differ
 
packages/dictionaries/brazil.py CHANGED
@@ -1,8 +1,14 @@
 
 
 
 
 
 
1
  # 巴西货币映射字典
2
  CURRENCY_MAP = {
3
- "BRL": "BRL", # 巴西雷亚尔
4
  "REAL": "BRL",
5
- "USD": "USD", # 美元
6
  "DÓLAR": "USD",
7
  }
8
 
@@ -16,12 +22,166 @@ UNIT_MAP = {
16
  "UN": "PCS",
17
  }
18
 
19
- # 巴西运输方式映射
20
  TRANSPORT_MODE_MAP = {
21
  "MARITIMO": "SEA",
22
  "MARÍTIMO": "SEA",
 
 
23
  "AEREO": "AIR",
24
  "AÉREO": "AIR",
 
 
25
  "RODOVIARIO": "ROAD",
26
  "RODOVIÁRIO": "ROAD",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import io
3
+ from typing import Dict, Optional
4
+
5
+ from packages.core.http_client import http_client
6
+
7
  # 巴西货币映射字典
8
  CURRENCY_MAP = {
9
+ "BRL": "BRL",
10
  "REAL": "BRL",
11
+ "USD": "USD",
12
  "DÓLAR": "USD",
13
  }
14
 
 
22
  "UN": "PCS",
23
  }
24
 
25
+ # 巴西运输方式名称到标准方式的归一化映射
26
  TRANSPORT_MODE_MAP = {
27
  "MARITIMO": "SEA",
28
  "MARÍTIMO": "SEA",
29
+ "MARITIMA": "SEA",
30
+ "MARÍTIMA": "SEA",
31
  "AEREO": "AIR",
32
  "AÉREO": "AIR",
33
+ "AEREA": "AIR",
34
+ "AÉREA": "AIR",
35
  "RODOVIARIO": "ROAD",
36
  "RODOVIÁRIO": "ROAD",
37
+ "RODOVIARIA": "ROAD",
38
+ "RODOVIÁRIA": "ROAD",
39
+ "FERROVIARIO": "RAIL",
40
+ "FERROVIÁRIO": "RAIL",
41
+ "FERROVIARIA": "RAIL",
42
+ "FERROVIÁRIA": "RAIL",
43
+ "FLUVIAL": "WATERWAY",
44
+ "LACUSTRE": "WATERWAY",
45
+ "POSTAL": "POSTAL",
46
+ "COURIER": "COURIER",
47
+ "DUTOVIARIO": "PIPELINE",
48
+ "DUTOVIÁRIO": "PIPELINE",
49
+ "POR REBOQUE": "TOWED",
50
+ "ENTRADA/SAIDA FICTA": "FICTITIOUS",
51
+ "ENTRADA/SAÍDA FICTA": "FICTITIOUS",
52
+ "VIA DESCONHECIDA": "UNKNOWN",
53
  }
54
+
55
+ BRAZIL_TABLE_BASE_URL = "https://balanca.economia.gov.br/balanca/bd/tabelas"
56
+
57
+ _COUNTRY_ALPHA3_BY_CODE: Optional[Dict[str, str]] = None
58
+ _URF_NAME_BY_CODE: Optional[Dict[str, str]] = None
59
+ _UF_NAME_BY_CODE: Optional[Dict[str, str]] = None
60
+ _VIA_NAME_BY_CODE: Optional[Dict[str, str]] = None
61
+ _NCM_NAME_BY_CODE: Optional[Dict[str, str]] = None
62
+
63
+
64
+ async def get_country_alpha3(code: Optional[str]) -> Optional[str]:
65
+ if not code:
66
+ return None
67
+ lookup = await _get_country_lookup()
68
+ normalized = _normalize_code(code)
69
+ return lookup.get(normalized)
70
+
71
+
72
+ async def get_urf_name(code: Optional[str]) -> Optional[str]:
73
+ if not code:
74
+ return None
75
+ lookup = await _get_urf_lookup()
76
+ normalized = _normalize_code(code)
77
+ return lookup.get(normalized)
78
+
79
+
80
+ async def get_state_name(code: Optional[str]) -> Optional[str]:
81
+ if not code:
82
+ return None
83
+ lookup = await _get_uf_lookup()
84
+ return lookup.get(str(code).strip().upper())
85
+
86
+
87
+ async def get_transport_mode(code: Optional[str]) -> str:
88
+ if not code:
89
+ return "UNKNOWN"
90
+ lookup = await _get_via_lookup()
91
+ via_name = lookup.get(_normalize_code(code), "")
92
+ normalized_name = via_name.upper()
93
+ if normalized_name in TRANSPORT_MODE_MAP:
94
+ return TRANSPORT_MODE_MAP[normalized_name]
95
+
96
+ for keyword, standard_mode in TRANSPORT_MODE_MAP.items():
97
+ if keyword and keyword in normalized_name:
98
+ return standard_mode
99
+
100
+ return "UNKNOWN"
101
+
102
+
103
+ async def get_ncm_name(code: Optional[str]) -> Optional[str]:
104
+ if not code:
105
+ return None
106
+ lookup = await _get_ncm_lookup()
107
+ normalized = _normalize_code(code)
108
+ return lookup.get(normalized)
109
+
110
+
111
+ async def _get_country_lookup() -> Dict[str, str]:
112
+ global _COUNTRY_ALPHA3_BY_CODE
113
+ if _COUNTRY_ALPHA3_BY_CODE is None:
114
+ rows = await _load_csv_rows("PAIS.csv")
115
+ _COUNTRY_ALPHA3_BY_CODE = {
116
+ _normalize_code(row.get("CO_PAIS")): (row.get("CO_PAIS_ISOA3") or "").strip().upper()
117
+ for row in rows
118
+ if row.get("CO_PAIS")
119
+ }
120
+ return _COUNTRY_ALPHA3_BY_CODE
121
+
122
+
123
+ async def _get_urf_lookup() -> Dict[str, str]:
124
+ global _URF_NAME_BY_CODE
125
+ if _URF_NAME_BY_CODE is None:
126
+ rows = await _load_csv_rows("URF.csv")
127
+ _URF_NAME_BY_CODE = {
128
+ _normalize_code(row.get("CO_URF")): (row.get("NO_URF") or "").strip()
129
+ for row in rows
130
+ if row.get("CO_URF")
131
+ }
132
+ return _URF_NAME_BY_CODE
133
+
134
+
135
+ async def _get_uf_lookup() -> Dict[str, str]:
136
+ global _UF_NAME_BY_CODE
137
+ if _UF_NAME_BY_CODE is None:
138
+ rows = await _load_csv_rows("UF.csv")
139
+ _UF_NAME_BY_CODE = {
140
+ (row.get("SG_UF") or "").strip().upper(): (row.get("NO_UF") or "").strip()
141
+ for row in rows
142
+ if row.get("SG_UF")
143
+ }
144
+ return _UF_NAME_BY_CODE
145
+
146
+
147
+ async def _get_via_lookup() -> Dict[str, str]:
148
+ global _VIA_NAME_BY_CODE
149
+ if _VIA_NAME_BY_CODE is None:
150
+ rows = await _load_csv_rows("VIA.csv")
151
+ _VIA_NAME_BY_CODE = {
152
+ _normalize_code(row.get("CO_VIA")): (row.get("NO_VIA") or "").strip()
153
+ for row in rows
154
+ if row.get("CO_VIA")
155
+ }
156
+ return _VIA_NAME_BY_CODE
157
+
158
+
159
+ async def _get_ncm_lookup() -> Dict[str, str]:
160
+ global _NCM_NAME_BY_CODE
161
+ if _NCM_NAME_BY_CODE is None:
162
+ rows = await _load_csv_rows("NCM.csv")
163
+ _NCM_NAME_BY_CODE = {
164
+ _normalize_code(row.get("CO_NCM")): (row.get("NO_NCM_POR") or "").strip()
165
+ for row in rows
166
+ if row.get("CO_NCM")
167
+ }
168
+ return _NCM_NAME_BY_CODE
169
+
170
+
171
+ async def _load_csv_rows(file_name: str) -> list[dict]:
172
+ url = f"{BRAZIL_TABLE_BASE_URL}/{file_name}"
173
+ response = await http_client.get(
174
+ url,
175
+ headers={"User-Agent": "customs-data/1.0"},
176
+ timeout=180,
177
+ follow_redirects=True,
178
+ allow_insecure_fallback=True,
179
+ )
180
+ text = response.content.decode("latin-1", errors="ignore")
181
+ return list(csv.DictReader(io.StringIO(text), delimiter=";"))
182
+
183
+
184
+ def _normalize_code(value: Optional[str]) -> str:
185
+ if value is None:
186
+ return ""
187
+ return str(value).replace('"', "").strip()