douanecode-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- douanecode_mcp-0.1.0/Dockerfile +13 -0
- douanecode_mcp-0.1.0/PKG-INFO +71 -0
- douanecode_mcp-0.1.0/README.md +56 -0
- douanecode_mcp-0.1.0/deploy/k8s.yaml +68 -0
- douanecode_mcp-0.1.0/pyproject.toml +33 -0
- douanecode_mcp-0.1.0/src/douanecode_mcp/__init__.py +0 -0
- douanecode_mcp-0.1.0/src/douanecode_mcp/server.py +140 -0
- douanecode_mcp-0.1.0/tests/test_server.py +79 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY pyproject.toml README.md ./
|
|
5
|
+
COPY src ./src
|
|
6
|
+
RUN pip install --no-cache-dir .
|
|
7
|
+
|
|
8
|
+
ENV MCP_TRANSPORT=streamable-http \
|
|
9
|
+
FASTMCP_HOST=0.0.0.0 \
|
|
10
|
+
FASTMCP_PORT=8000
|
|
11
|
+
|
|
12
|
+
EXPOSE 8000
|
|
13
|
+
CMD ["douanecode-mcp"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: douanecode-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server exposing DouaneCode customs code search (HS/NC8/TARIC) to AI agents
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: ai-agents,customs,douane,hs-code,mcp,taric
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: mcp>=1.2.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# douanecode-mcp
|
|
17
|
+
|
|
18
|
+
Serveur MCP officiel de [DouaneCode.fr](https://douanecode.fr) : donne aux
|
|
19
|
+
agents IA (Claude, Cursor, agents logistique/ERP/e-commerce) un accès outillé à
|
|
20
|
+
la recherche de codes douaniers HS/NC8/TARIC, aux taux de droits et à la veille
|
|
21
|
+
tarifaire.
|
|
22
|
+
|
|
23
|
+
**Modèle économique** : chaque appel exige une clé API DouaneCode (plan Pro,
|
|
24
|
+
99 €/an — tier Cabinet à venir). Le serveur est un wrapper mince ; le backend
|
|
25
|
+
existant applique authentification, rate limiting par plan et analytics.
|
|
26
|
+
|
|
27
|
+
## Outils exposés
|
|
28
|
+
|
|
29
|
+
| Outil | Description |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `search_customs_code` | Description produit en langage naturel → codes candidats |
|
|
32
|
+
| `get_code_details` | Détail d'un code (libellés, hiérarchie, unités) |
|
|
33
|
+
| `get_duty_rates` | Droits de douane + mesures, option pays d'origine |
|
|
34
|
+
| `get_tariff_changes` | Changements tarifaires récents (veille) |
|
|
35
|
+
|
|
36
|
+
## Usage local (Claude Desktop / Claude Code / Cursor)
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"douanecode": {
|
|
42
|
+
"command": "uvx",
|
|
43
|
+
"args": ["douanecode-mcp"],
|
|
44
|
+
"env": { "DOUANECODE_API_KEY": "dc_..." }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Clé API : https://douanecode.fr/compte (plan Pro requis).
|
|
51
|
+
|
|
52
|
+
## Dev
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e ".[dev]"
|
|
56
|
+
pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Déploiement hébergé
|
|
60
|
+
|
|
61
|
+
`Dockerfile` + `deploy/k8s.yaml` (mcp.douanecode.fr, transport streamable-http).
|
|
62
|
+
Voir la note dans le manifeste : le mode hébergé nécessite de lire la clé API
|
|
63
|
+
depuis les headers client (TODO) ; le mode stdio publié sur PyPI fonctionne
|
|
64
|
+
sans cela.
|
|
65
|
+
|
|
66
|
+
## Distribution (go-to-market)
|
|
67
|
+
|
|
68
|
+
1. Publier sur PyPI (`douanecode-mcp`) → utilisable via `uvx` immédiatement.
|
|
69
|
+
2. Lister sur les annuaires MCP (mcp.so, Smithery, PulseMCP, Glama) +
|
|
70
|
+
registre officiel `registry.modelcontextprotocol.io`.
|
|
71
|
+
3. Page https://douanecode.fr/mcp expliquant l'installation → conversion Pro.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# douanecode-mcp
|
|
2
|
+
|
|
3
|
+
Serveur MCP officiel de [DouaneCode.fr](https://douanecode.fr) : donne aux
|
|
4
|
+
agents IA (Claude, Cursor, agents logistique/ERP/e-commerce) un accès outillé à
|
|
5
|
+
la recherche de codes douaniers HS/NC8/TARIC, aux taux de droits et à la veille
|
|
6
|
+
tarifaire.
|
|
7
|
+
|
|
8
|
+
**Modèle économique** : chaque appel exige une clé API DouaneCode (plan Pro,
|
|
9
|
+
99 €/an — tier Cabinet à venir). Le serveur est un wrapper mince ; le backend
|
|
10
|
+
existant applique authentification, rate limiting par plan et analytics.
|
|
11
|
+
|
|
12
|
+
## Outils exposés
|
|
13
|
+
|
|
14
|
+
| Outil | Description |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `search_customs_code` | Description produit en langage naturel → codes candidats |
|
|
17
|
+
| `get_code_details` | Détail d'un code (libellés, hiérarchie, unités) |
|
|
18
|
+
| `get_duty_rates` | Droits de douane + mesures, option pays d'origine |
|
|
19
|
+
| `get_tariff_changes` | Changements tarifaires récents (veille) |
|
|
20
|
+
|
|
21
|
+
## Usage local (Claude Desktop / Claude Code / Cursor)
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"douanecode": {
|
|
27
|
+
"command": "uvx",
|
|
28
|
+
"args": ["douanecode-mcp"],
|
|
29
|
+
"env": { "DOUANECODE_API_KEY": "dc_..." }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Clé API : https://douanecode.fr/compte (plan Pro requis).
|
|
36
|
+
|
|
37
|
+
## Dev
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e ".[dev]"
|
|
41
|
+
pytest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Déploiement hébergé
|
|
45
|
+
|
|
46
|
+
`Dockerfile` + `deploy/k8s.yaml` (mcp.douanecode.fr, transport streamable-http).
|
|
47
|
+
Voir la note dans le manifeste : le mode hébergé nécessite de lire la clé API
|
|
48
|
+
depuis les headers client (TODO) ; le mode stdio publié sur PyPI fonctionne
|
|
49
|
+
sans cela.
|
|
50
|
+
|
|
51
|
+
## Distribution (go-to-market)
|
|
52
|
+
|
|
53
|
+
1. Publier sur PyPI (`douanecode-mcp`) → utilisable via `uvx` immédiatement.
|
|
54
|
+
2. Lister sur les annuaires MCP (mcp.so, Smithery, PulseMCP, Glama) +
|
|
55
|
+
registre officiel `registry.modelcontextprotocol.io`.
|
|
56
|
+
3. Page https://douanecode.fr/mcp expliquant l'installation → conversion Pro.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# douanecode-mcp — serveur MCP hébergé (streamable-http) sur mcp.douanecode.fr
|
|
2
|
+
# NB : en mode hébergé, la clé API vient du CLIENT (header X-API-Key transmis
|
|
3
|
+
# par l'agent via la config MCP) — à terme, adapter le serveur pour lire la clé
|
|
4
|
+
# depuis les headers de la requête MCP plutôt que l'env. Le déploiement stdio
|
|
5
|
+
# local (Claude Desktop/Code, Cursor) fonctionne dès aujourd'hui sans ceci.
|
|
6
|
+
apiVersion: apps/v1
|
|
7
|
+
kind: Deployment
|
|
8
|
+
metadata:
|
|
9
|
+
name: douanecode-mcp
|
|
10
|
+
namespace: douanecode-dev
|
|
11
|
+
spec:
|
|
12
|
+
replicas: 1
|
|
13
|
+
selector:
|
|
14
|
+
matchLabels:
|
|
15
|
+
app: douanecode-mcp
|
|
16
|
+
template:
|
|
17
|
+
metadata:
|
|
18
|
+
labels:
|
|
19
|
+
app: douanecode-mcp
|
|
20
|
+
spec:
|
|
21
|
+
containers:
|
|
22
|
+
- name: douanecode-mcp
|
|
23
|
+
image: harbor.homalab.com/douanecode/mcp:latest
|
|
24
|
+
ports:
|
|
25
|
+
- containerPort: 8000
|
|
26
|
+
env:
|
|
27
|
+
- name: MCP_TRANSPORT
|
|
28
|
+
value: streamable-http
|
|
29
|
+
- name: DOUANECODE_API_URL
|
|
30
|
+
value: http://website-backend.douanecode-dev.svc.cluster.local:8000
|
|
31
|
+
resources:
|
|
32
|
+
requests: { cpu: 50m, memory: 96Mi }
|
|
33
|
+
limits: { cpu: 250m, memory: 256Mi }
|
|
34
|
+
---
|
|
35
|
+
apiVersion: v1
|
|
36
|
+
kind: Service
|
|
37
|
+
metadata:
|
|
38
|
+
name: douanecode-mcp
|
|
39
|
+
namespace: douanecode-dev
|
|
40
|
+
spec:
|
|
41
|
+
selector:
|
|
42
|
+
app: douanecode-mcp
|
|
43
|
+
ports:
|
|
44
|
+
- port: 80
|
|
45
|
+
targetPort: 8000
|
|
46
|
+
---
|
|
47
|
+
apiVersion: networking.k8s.io/v1
|
|
48
|
+
kind: Ingress
|
|
49
|
+
metadata:
|
|
50
|
+
name: douanecode-mcp
|
|
51
|
+
namespace: douanecode-dev
|
|
52
|
+
annotations:
|
|
53
|
+
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
54
|
+
spec:
|
|
55
|
+
ingressClassName: nginx
|
|
56
|
+
tls:
|
|
57
|
+
- hosts: [mcp.douanecode.fr]
|
|
58
|
+
secretName: douanecode-mcp-tls
|
|
59
|
+
rules:
|
|
60
|
+
- host: mcp.douanecode.fr
|
|
61
|
+
http:
|
|
62
|
+
paths:
|
|
63
|
+
- path: /
|
|
64
|
+
pathType: Prefix
|
|
65
|
+
backend:
|
|
66
|
+
service:
|
|
67
|
+
name: douanecode-mcp
|
|
68
|
+
port: { number: 80 }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "douanecode-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server exposing DouaneCode customs code search (HS/NC8/TARIC) to AI agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
keywords = ["mcp", "customs", "taric", "hs-code", "douane", "ai-agents"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"mcp>=1.2.0",
|
|
11
|
+
"httpx>=0.27",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = [
|
|
16
|
+
"pytest>=8.0",
|
|
17
|
+
"pytest-asyncio>=0.23",
|
|
18
|
+
"respx>=0.21",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
douanecode-mcp = "douanecode_mcp.server:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/douanecode_mcp"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
asyncio_mode = "auto"
|
|
33
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""DouaneCode MCP server.
|
|
2
|
+
|
|
3
|
+
Expose la recherche de codes douaniers (HS/NC8/TARIC) de douanecode.fr comme
|
|
4
|
+
outils MCP pour agents IA. Chaque appel est authentifié par la clé API du
|
|
5
|
+
client (plan Pro) via le header X-API-Key ; le backend applique le rate
|
|
6
|
+
limiting et la facturation par plan.
|
|
7
|
+
|
|
8
|
+
Transports :
|
|
9
|
+
- stdio (défaut) : usage local (Claude Desktop, Claude Code, Cursor...)
|
|
10
|
+
- streamable-http : hébergé (mcp.douanecode.fr), MCP_TRANSPORT=streamable-http
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
|
|
20
|
+
API_BASE_URL = os.environ.get("DOUANECODE_API_URL", "https://douanecode.fr/api")
|
|
21
|
+
API_KEY_ENV = "DOUANECODE_API_KEY"
|
|
22
|
+
TIMEOUT_SECONDS = 15.0
|
|
23
|
+
|
|
24
|
+
mcp = FastMCP(
|
|
25
|
+
"douanecode",
|
|
26
|
+
instructions=(
|
|
27
|
+
"Recherche officielle de codes douaniers français/UE (HS, NC8, TARIC) : "
|
|
28
|
+
"classement tarifaire en langage naturel, taux de droits, mesures TARIC "
|
|
29
|
+
"et changements tarifaires. Nécessite une clé API DouaneCode Pro "
|
|
30
|
+
f"(variable d'environnement {API_KEY_ENV}, obtenue sur "
|
|
31
|
+
"https://douanecode.fr/compte)."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DouaneCodeError(Exception):
|
|
37
|
+
"""Erreur API remontée telle quelle à l'agent appelant."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _api_key() -> str:
|
|
41
|
+
key = os.environ.get(API_KEY_ENV, "").strip()
|
|
42
|
+
if not key:
|
|
43
|
+
raise DouaneCodeError(
|
|
44
|
+
f"Clé API absente : définissez {API_KEY_ENV}. "
|
|
45
|
+
"Créez une clé (plan Pro) sur https://douanecode.fr/compte."
|
|
46
|
+
)
|
|
47
|
+
return key
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _get(path: str, params: Optional[dict[str, Any]] = None) -> Any:
|
|
51
|
+
async with httpx.AsyncClient(
|
|
52
|
+
base_url=API_BASE_URL, timeout=TIMEOUT_SECONDS
|
|
53
|
+
) as client:
|
|
54
|
+
response = await client.get(
|
|
55
|
+
path,
|
|
56
|
+
params={k: v for k, v in (params or {}).items() if v is not None},
|
|
57
|
+
headers={"X-API-Key": _api_key()},
|
|
58
|
+
)
|
|
59
|
+
if response.status_code == 429:
|
|
60
|
+
raise DouaneCodeError(
|
|
61
|
+
"Limite quotidienne du plan atteinte. "
|
|
62
|
+
"Passez à un plan supérieur sur https://douanecode.fr/tarifs."
|
|
63
|
+
)
|
|
64
|
+
if response.status_code in (401, 403):
|
|
65
|
+
raise DouaneCodeError(
|
|
66
|
+
"Clé API invalide ou plan insuffisant (Pro requis) : "
|
|
67
|
+
"https://douanecode.fr/tarifs."
|
|
68
|
+
)
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
return response.json()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@mcp.tool()
|
|
74
|
+
async def search_customs_code(
|
|
75
|
+
query: str,
|
|
76
|
+
limit: int = 10,
|
|
77
|
+
chapter: Optional[str] = None,
|
|
78
|
+
leaf_only: bool = True,
|
|
79
|
+
) -> Any:
|
|
80
|
+
"""Trouve les codes douaniers (HS/NC8/TARIC) correspondant à une description
|
|
81
|
+
de produit en langage naturel.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
query: Description du produit (ex. "carte électronique pour trottinette",
|
|
85
|
+
"fromage râpé sous vide"). Français ou anglais.
|
|
86
|
+
limit: Nombre maximum de candidats retournés (1-100).
|
|
87
|
+
chapter: Filtre optionnel sur un chapitre SH à 2 chiffres (ex. "85").
|
|
88
|
+
leaf_only: Ne retourner que les codes feuilles déclarables (recommandé).
|
|
89
|
+
"""
|
|
90
|
+
return await _get(
|
|
91
|
+
"/search",
|
|
92
|
+
{"q": query, "limit": limit, "chapter": chapter, "leaf_only": leaf_only},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
async def get_code_details(code: str) -> Any:
|
|
98
|
+
"""Retourne le détail d'un code douanier : libellés, hiérarchie, unités,
|
|
99
|
+
et informations de classement.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
code: Code SH/NC8/TARIC (4 à 10 chiffres, ex. "8517620000").
|
|
103
|
+
"""
|
|
104
|
+
return await _get(f"/code/{code.strip()}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@mcp.tool()
|
|
108
|
+
async def get_duty_rates(code: str, origin: Optional[str] = None) -> Any:
|
|
109
|
+
"""Retourne les taux de droits de douane et mesures applicables à un code
|
|
110
|
+
(droits tiers, préférences, restrictions).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
code: Code NC8/TARIC (8 à 10 chiffres).
|
|
114
|
+
origin: Code pays d'origine ISO-2 optionnel (ex. "CN", "US") pour les
|
|
115
|
+
taux préférentiels.
|
|
116
|
+
"""
|
|
117
|
+
return await _get(f"/duty/{code.strip()}", {"origin": origin})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@mcp.tool()
|
|
121
|
+
async def get_tariff_changes(limit: int = 20) -> Any:
|
|
122
|
+
"""Liste les changements tarifaires récents (nomenclature, taux, mesures) —
|
|
123
|
+
utile pour la veille réglementaire douanière.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
limit: Nombre maximum de changements retournés.
|
|
127
|
+
"""
|
|
128
|
+
return await _get("/tariff-changes", {"limit": limit})
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main() -> None:
|
|
132
|
+
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
|
133
|
+
if transport not in ("stdio", "streamable-http", "sse"):
|
|
134
|
+
print(f"Transport MCP inconnu : {transport}", file=sys.stderr)
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
mcp.run(transport=transport)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import respx
|
|
5
|
+
from httpx import Response
|
|
6
|
+
|
|
7
|
+
from douanecode_mcp import server
|
|
8
|
+
from douanecode_mcp.server import DouaneCodeError, mcp
|
|
9
|
+
|
|
10
|
+
API = "https://douanecode.fr/api"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(autouse=True)
|
|
14
|
+
def api_key(monkeypatch):
|
|
15
|
+
monkeypatch.setenv(server.API_KEY_ENV, "dc_test_key")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def _call(tool_name: str, args: dict):
|
|
19
|
+
result = await mcp.call_tool(tool_name, args)
|
|
20
|
+
# FastMCP returns (content_blocks, raw_result) or content blocks only
|
|
21
|
+
blocks = result[0] if isinstance(result, tuple) else result
|
|
22
|
+
return json.loads(blocks[0].text)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_all_tools_registered():
|
|
26
|
+
tools = {t.name for t in mcp._tool_manager.list_tools()}
|
|
27
|
+
assert tools == {
|
|
28
|
+
"search_customs_code",
|
|
29
|
+
"get_code_details",
|
|
30
|
+
"get_duty_rates",
|
|
31
|
+
"get_tariff_changes",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@respx.mock
|
|
36
|
+
async def test_search_sends_api_key_and_params():
|
|
37
|
+
route = respx.get(f"{API}/search").mock(
|
|
38
|
+
return_value=Response(200, json={"results": [{"code": "85176200"}]})
|
|
39
|
+
)
|
|
40
|
+
payload = await _call(
|
|
41
|
+
"search_customs_code", {"query": "carte électronique", "limit": 5}
|
|
42
|
+
)
|
|
43
|
+
assert payload["results"][0]["code"] == "85176200"
|
|
44
|
+
request = route.calls.last.request
|
|
45
|
+
assert request.headers["X-API-Key"] == "dc_test_key"
|
|
46
|
+
assert request.url.params["q"] == "carte électronique"
|
|
47
|
+
assert request.url.params["limit"] == "5"
|
|
48
|
+
# chapter=None ne doit pas être envoyé
|
|
49
|
+
assert "chapter" not in request.url.params
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@respx.mock
|
|
53
|
+
async def test_duty_rates_with_origin():
|
|
54
|
+
route = respx.get(f"{API}/duty/85176200").mock(
|
|
55
|
+
return_value=Response(200, json={"duty": "3.7%"})
|
|
56
|
+
)
|
|
57
|
+
payload = await _call("get_duty_rates", {"code": " 85176200 ", "origin": "CN"})
|
|
58
|
+
assert payload["duty"] == "3.7%"
|
|
59
|
+
assert route.calls.last.request.url.params["origin"] == "CN"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@respx.mock
|
|
63
|
+
async def test_rate_limit_maps_to_upsell_error():
|
|
64
|
+
respx.get(f"{API}/search").mock(return_value=Response(429, json={}))
|
|
65
|
+
with pytest.raises(DouaneCodeError, match="tarifs"):
|
|
66
|
+
await server.search_customs_code(query="test")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@respx.mock
|
|
70
|
+
async def test_invalid_key_maps_to_auth_error():
|
|
71
|
+
respx.get(f"{API}/code/8517").mock(return_value=Response(401, json={}))
|
|
72
|
+
with pytest.raises(DouaneCodeError, match="Pro requis"):
|
|
73
|
+
await server.get_code_details(code="8517")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def test_missing_api_key_raises(monkeypatch):
|
|
77
|
+
monkeypatch.delenv(server.API_KEY_ENV, raising=False)
|
|
78
|
+
with pytest.raises(DouaneCodeError, match="DOUANECODE_API_KEY"):
|
|
79
|
+
await server.get_tariff_changes()
|