makea-cli 0.2.14__tar.gz → 0.2.15__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.
- {makea_cli-0.2.14 → makea_cli-0.2.15}/CHANGELOG.md +28 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/PKG-INFO +2 -2
- {makea_cli-0.2.14 → makea_cli-0.2.15}/README.md +1 -1
- makea_cli-0.2.15/makea_cli/__init__.py +1 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/api_client.py +12 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/metrics/cmd.py +27 -65
- {makea_cli-0.2.14 → makea_cli-0.2.15}/pyproject.toml +1 -1
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/golden_manifest.json +3 -3
- makea_cli-0.2.15/tests/test_currency_conversion.py +106 -0
- makea_cli-0.2.14/makea_cli/__init__.py +0 -1
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/.gitignore +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/__main__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/broker.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/analytics/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/brand/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/brand/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/factory/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/factory/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/orders/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/commands/supplier/cmd.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/config.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/errors.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/identity.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/main.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/output.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/posthog_client.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/makea_cli/registry.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/test_broker.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/test_output_contract.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/test_persona_commands.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/test_pm_reads.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/test_registry.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/tests/test_supplier_scorecard.py +0 -0
- {makea_cli-0.2.14 → makea_cli-0.2.15}/uv.lock +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.15
|
|
4
|
+
|
|
5
|
+
**Behaviour change: `admin get currency-conversion` returns different
|
|
6
|
+
numbers.** This is not only a bug fix. Anyone who reconciled figures with
|
|
7
|
+
this command should expect them to move, by up to several percent per
|
|
8
|
+
currency. The same call on 2026-10-16:
|
|
9
|
+
|
|
10
|
+
| GBP per | 0.2.14 | 0.2.15 | 1,000 converted, 0.2.14 → 0.2.15 |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| CNY | 0.12 | 0.113 | 120.00 → 113.00 (−5.8%) |
|
|
13
|
+
| USD | 0.76 | 0.752 | 760.00 → 752.00 |
|
|
14
|
+
| EUR | 0.85 | 0.848 | 850.00 → 848.00 |
|
|
15
|
+
|
|
16
|
+
What changed:
|
|
17
|
+
|
|
18
|
+
- Rates now come from the platform (`GET /admin/fx/rates`, backend
|
|
19
|
+
2026-10-16) instead of the CLI fetching the free FX source itself. They are
|
|
20
|
+
rounded **up to 3 decimals**, as the platform rounds them; 0.2.14 rounded
|
|
21
|
+
up to 2, which overstated cheap currencies such as CNY most.
|
|
22
|
+
- With one `--from` and `--amount`, `converted` is the platform's own
|
|
23
|
+
conversion, rounded **up** to the cent; 0.2.14 rounded to the nearest cent.
|
|
24
|
+
- So the numbers now match the platform's prices and revenue metrics, which
|
|
25
|
+
the 0.2.14 ones did not.
|
|
26
|
+
- Output keys are unchanged except `rounding` (now `"up"`, was
|
|
27
|
+
`"ceil_2dp"`), plus new `rate_decimals` and `unavailable`. A currency with
|
|
28
|
+
no rate still fails the command. Needs an admin login, like every `admin`
|
|
29
|
+
command.
|
|
30
|
+
|
|
3
31
|
## 0.2.14
|
|
4
32
|
|
|
5
33
|
- `admin update tracking-status`: help text no longer describes it as "the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: makea-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: Makea admin CLI (browser login + Makea admin HTTP API)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: click<9,>=8.1
|
|
@@ -100,7 +100,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
100
100
|
| `makea-cli admin generate pricing-groups` | write(ask) | `POST /admin/production_orders/{id}/details/update` | Lay a production order's variants into pricing groups along one axis (--by style \| colour \| variant \| all) with th… | `production-groups-generate` |
|
|
101
101
|
| `makea-cli admin get active-users` | read | `POST /admin/analytics/query` | Distinct active users per day over the last N days (DAU series). | `analytics-active-users` |
|
|
102
102
|
| `makea-cli admin get analytics-query` | read | `POST /admin/analytics/query` | Run an arbitrary HogQL query (escape hatch). Example: analytics-query "SELECT count() FROM events" | `analytics-query` |
|
|
103
|
-
| `makea-cli admin get currency-conversion` | read | `
|
|
103
|
+
| `makea-cli admin get currency-conversion` | read | `GET /admin/fx/rates` | the platform's own exchange rates and rounding (rate table for several --from; one --from with --amount converts it). | `convert-currency` |
|
|
104
104
|
| `makea-cli admin get feature-usage` | read | `POST /admin/analytics/query` | How many distinct users used a feature recently, plus event volume. Matches $pageview path and feature_time_spent eve… | `analytics-feature-usage` |
|
|
105
105
|
| `makea-cli admin get library-schema` | read | `GET /admin/library_components/schema` | the tech-pack library taxonomy: sections and their library-role field keys (run this before create/update). | `library-schema` |
|
|
106
106
|
| `makea-cli admin get production-pricing` | read | `GET /admin/production_orders/{id}/details` | show a production order's price tiers, MOQ, sizes/colours, pricing groups and cart coverage. | `production-pricing-show` |
|
|
@@ -88,7 +88,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
88
88
|
| `makea-cli admin generate pricing-groups` | write(ask) | `POST /admin/production_orders/{id}/details/update` | Lay a production order's variants into pricing groups along one axis (--by style \| colour \| variant \| all) with th… | `production-groups-generate` |
|
|
89
89
|
| `makea-cli admin get active-users` | read | `POST /admin/analytics/query` | Distinct active users per day over the last N days (DAU series). | `analytics-active-users` |
|
|
90
90
|
| `makea-cli admin get analytics-query` | read | `POST /admin/analytics/query` | Run an arbitrary HogQL query (escape hatch). Example: analytics-query "SELECT count() FROM events" | `analytics-query` |
|
|
91
|
-
| `makea-cli admin get currency-conversion` | read | `
|
|
91
|
+
| `makea-cli admin get currency-conversion` | read | `GET /admin/fx/rates` | the platform's own exchange rates and rounding (rate table for several --from; one --from with --amount converts it). | `convert-currency` |
|
|
92
92
|
| `makea-cli admin get feature-usage` | read | `POST /admin/analytics/query` | How many distinct users used a feature recently, plus event volume. Matches $pageview path and feature_time_spent eve… | `analytics-feature-usage` |
|
|
93
93
|
| `makea-cli admin get library-schema` | read | `GET /admin/library_components/schema` | the tech-pack library taxonomy: sections and their library-role field keys (run this before create/update). | `library-schema` |
|
|
94
94
|
| `makea-cli admin get production-pricing` | read | `GET /admin/production_orders/{id}/details` | show a production order's price tiers, MOQ, sizes/colours, pricing groups and cart coverage. | `production-pricing-show` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.15"
|
|
@@ -844,6 +844,18 @@ def get_cumulative_payment_and_platform_revenue(
|
|
|
844
844
|
)
|
|
845
845
|
|
|
846
846
|
|
|
847
|
+
def get_fx_rates(*, to: str, sources: list[str], amount: float | None = None) -> Any:
|
|
848
|
+
"""
|
|
849
|
+
GET /admin/fx/rates — the platform's own exchange rates (rounded up to its
|
|
850
|
+
RATE_DECIMALS), and with one source and an amount, that amount converted the
|
|
851
|
+
way the platform converts it.
|
|
852
|
+
"""
|
|
853
|
+
params: dict[str, Any] = {"to": to, "from": ",".join(sources)}
|
|
854
|
+
if amount is not None:
|
|
855
|
+
params["amount"] = amount
|
|
856
|
+
return get_json("/admin/fx/rates", params=params)
|
|
857
|
+
|
|
858
|
+
|
|
847
859
|
def extract_ai_tech_specifications(product_reference_id: str, *, timeout: float = 300.0) -> Any:
|
|
848
860
|
"""
|
|
849
861
|
GET /admin/products/{id}/extract-tech-specifications — synchronous AI extraction;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import math
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
3
|
import typer
|
|
7
4
|
from rich.console import Console
|
|
8
5
|
|
|
@@ -16,38 +13,6 @@ console = Console()
|
|
|
16
13
|
_GRANULARITIES = ("day", "week", "month")
|
|
17
14
|
_ORDER_TYPES = ("all", "sampling", "production")
|
|
18
15
|
|
|
19
|
-
# Same free FX source the backend uses (FreeCurrencyService), so converted
|
|
20
|
-
# amounts match the platform's revenue metrics.
|
|
21
|
-
_FX_PRIMARY = (
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/{base}.json"
|
|
23
|
-
)
|
|
24
|
-
_FX_FALLBACK = "https://latest.currency-api.pages.dev/v1/currencies/{base}.json"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _fx_rate(base: str, target: str) -> float:
|
|
28
|
-
"""Exchange rate base -> target, rounded UP to 2dp exactly like the backend
|
|
29
|
-
(math.ceil(raw * 100) / 100) so totals reconcile with revenue metrics."""
|
|
30
|
-
base, target = base.lower(), target.lower()
|
|
31
|
-
if base == target:
|
|
32
|
-
return 1.0
|
|
33
|
-
last_err: Exception | None = None
|
|
34
|
-
for url in (_FX_PRIMARY.format(base=base), _FX_FALLBACK.format(base=base)):
|
|
35
|
-
try:
|
|
36
|
-
with httpx.Client(timeout=10) as client:
|
|
37
|
-
resp = client.get(url)
|
|
38
|
-
resp.raise_for_status()
|
|
39
|
-
data = resp.json()
|
|
40
|
-
raw = data.get(base, {}).get(target)
|
|
41
|
-
if raw is not None:
|
|
42
|
-
return math.ceil(float(raw) * 100) / 100
|
|
43
|
-
except Exception as e: # noqa: BLE001 - try fallback URL
|
|
44
|
-
last_err = e
|
|
45
|
-
continue
|
|
46
|
-
raise RuntimeError(
|
|
47
|
-
f"FX rate {base.upper()}->{target.upper()} unavailable"
|
|
48
|
-
+ (f": {last_err}" if last_err else "")
|
|
49
|
-
)
|
|
50
|
-
|
|
51
16
|
|
|
52
17
|
def register(app: typer.Typer) -> None:
|
|
53
18
|
@command(
|
|
@@ -141,13 +106,13 @@ def register(app: typer.Typer) -> None:
|
|
|
141
106
|
"get",
|
|
142
107
|
"currency-conversion",
|
|
143
108
|
legacy="convert-currency",
|
|
144
|
-
api="GET
|
|
145
|
-
external="currency-api",
|
|
109
|
+
api="GET /admin/fx/rates",
|
|
146
110
|
help=(
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"one call, then convert
|
|
111
|
+
"GET /admin/fx/rates — convert money between currencies at the "
|
|
112
|
+
"platform's own rates and rounding, so totals reconcile with its "
|
|
113
|
+
"prices and revenue metrics. Pass several source currencies via --from "
|
|
114
|
+
"(comma-separated) to get a full rate table in one call, then convert "
|
|
115
|
+
"locally — avoids repeated lookups."
|
|
151
116
|
),
|
|
152
117
|
)
|
|
153
118
|
def convert_currency(
|
|
@@ -165,17 +130,20 @@ def register(app: typer.Typer) -> None:
|
|
|
165
130
|
help="Optional amount to convert (only meaningful with a single --from).",
|
|
166
131
|
),
|
|
167
132
|
) -> None:
|
|
168
|
-
"""Return source->target rates
|
|
169
|
-
|
|
133
|
+
"""Return source->target rates, and optionally a converted amount, as the
|
|
134
|
+
platform computes them.
|
|
170
135
|
|
|
171
136
|
Use it to collapse a mixed-currency set into one currency: collect the
|
|
172
137
|
distinct currencies, call once with --from a,b,c to get every rate, then
|
|
173
|
-
multiply locally.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{"
|
|
178
|
-
|
|
138
|
+
multiply locally. The rates are the platform's (FreeCurrencyService,
|
|
139
|
+
rounded up to ``rate_decimals``) and come from the backend rather than
|
|
140
|
+
being fetched and rounded here, so they cannot drift from it.
|
|
141
|
+
|
|
142
|
+
Output JSON: {"to", "rates": {CUR: rate, ...}, "rate_decimals",
|
|
143
|
+
"rounding", "source", "unavailable"} plus {"from", "rate", "amount",
|
|
144
|
+
"converted"} when a single --from and --amount are given; "converted" is
|
|
145
|
+
the platform's own conversion, rounding included. A currency with no
|
|
146
|
+
rate fails the command rather than coming back null.
|
|
179
147
|
"""
|
|
180
148
|
sources = [s.strip() for s in from_.split(",") if s.strip()]
|
|
181
149
|
if not sources:
|
|
@@ -183,22 +151,16 @@ def register(app: typer.Typer) -> None:
|
|
|
183
151
|
raise typer.Exit(1)
|
|
184
152
|
|
|
185
153
|
def _do() -> dict:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
"from": sources[0].upper(),
|
|
198
|
-
"rate": rate,
|
|
199
|
-
"amount": amount,
|
|
200
|
-
"converted": round(amount * rate, 2),
|
|
201
|
-
}
|
|
154
|
+
payload = api_client.get_fx_rates(
|
|
155
|
+
to=to,
|
|
156
|
+
sources=sources,
|
|
157
|
+
amount=amount if len(sources) == 1 else None,
|
|
158
|
+
)
|
|
159
|
+
out = (payload or {}).get("result") or {}
|
|
160
|
+
if out.get("unavailable"):
|
|
161
|
+
raise RuntimeError(
|
|
162
|
+
"FX rate unavailable: "
|
|
163
|
+
+ ", ".join(f"{s}->{to.upper()}" for s in out["unavailable"])
|
|
202
164
|
)
|
|
203
165
|
return out
|
|
204
166
|
|
|
@@ -980,8 +980,8 @@
|
|
|
980
980
|
"verb": "get",
|
|
981
981
|
"noun": "currency-conversion",
|
|
982
982
|
"access": "read",
|
|
983
|
-
"api": "GET
|
|
984
|
-
"external":
|
|
983
|
+
"api": "GET /admin/fx/rates",
|
|
984
|
+
"external": null,
|
|
985
985
|
"side_effects": [],
|
|
986
986
|
"approval": "auto",
|
|
987
987
|
"reversible": null,
|
|
@@ -990,7 +990,7 @@
|
|
|
990
990
|
"legacy": [
|
|
991
991
|
"convert-currency"
|
|
992
992
|
],
|
|
993
|
-
"help": "
|
|
993
|
+
"help": "GET /admin/fx/rates — convert money between currencies at the platform's own rates and rounding, so totals reconcile with its prices and revenue metrics. Pass several source currencies via --from (comma-separated) to get a full rate table in one call, then convert locally — avoids repeated lookups.",
|
|
994
994
|
"function": "makea_cli.commands.metrics.cmd.register.<locals>.convert_currency",
|
|
995
995
|
"flags": [
|
|
996
996
|
{
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""`admin get currency-conversion` reads the platform's rates from the backend.
|
|
2
|
+
|
|
3
|
+
It used to fetch the free FX source itself and round rates to 2 decimals and
|
|
4
|
+
amounts to the nearest cent, while the platform rounds rates up to 3 decimals
|
|
5
|
+
and amounts up to the cent -- so its totals drifted from the platform's. It
|
|
6
|
+
now asks GET /admin/fx/rates and returns what that says.
|
|
7
|
+
|
|
8
|
+
Run with ``.venv/bin/python -m unittest tests.test_currency_conversion``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import unittest
|
|
13
|
+
from unittest import mock
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from click.testing import CliRunner
|
|
17
|
+
|
|
18
|
+
from makea_cli import api_client, identity, output
|
|
19
|
+
from makea_cli.main import build_cli
|
|
20
|
+
|
|
21
|
+
RESULT = {
|
|
22
|
+
"to": "GBP",
|
|
23
|
+
"rates": {"USD": 0.752, "CNY": 0.113},
|
|
24
|
+
"unavailable": [],
|
|
25
|
+
"rate_decimals": 3,
|
|
26
|
+
"rounding": "up",
|
|
27
|
+
"source": "platform (FreeCurrencyService)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ApiClientTest(unittest.TestCase):
|
|
32
|
+
def test_rates(self):
|
|
33
|
+
with mock.patch.object(api_client, "get_json", return_value={}) as get:
|
|
34
|
+
api_client.get_fx_rates(to="gbp", sources=["usd", "cny"])
|
|
35
|
+
get.assert_called_once_with("/admin/fx/rates", params={"to": "gbp", "from": "usd,cny"})
|
|
36
|
+
|
|
37
|
+
def test_amount(self):
|
|
38
|
+
with mock.patch.object(api_client, "get_json", return_value={}) as get:
|
|
39
|
+
api_client.get_fx_rates(to="gbp", sources=["cny"], amount=250)
|
|
40
|
+
get.assert_called_once_with(
|
|
41
|
+
"/admin/fx/rates", params={"to": "gbp", "from": "cny", "amount": 250}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CommandTest(unittest.TestCase):
|
|
46
|
+
def setUp(self):
|
|
47
|
+
self.cli = build_cli()
|
|
48
|
+
self.runner = CliRunner()
|
|
49
|
+
output.state.force_json = True
|
|
50
|
+
|
|
51
|
+
def tearDown(self):
|
|
52
|
+
output.state.force_json = False
|
|
53
|
+
|
|
54
|
+
def _run(self, args, payload):
|
|
55
|
+
with (
|
|
56
|
+
mock.patch.object(identity, "current_persona", return_value="admin"),
|
|
57
|
+
mock.patch.object(api_client, "get_fx_rates", return_value=payload) as get,
|
|
58
|
+
# Nothing is fetched from an FX source directly any more.
|
|
59
|
+
mock.patch.object(httpx, "Client", side_effect=AssertionError("no network")),
|
|
60
|
+
):
|
|
61
|
+
result = self.runner.invoke(self.cli, ["admin", "get", "currency-conversion", *args])
|
|
62
|
+
return result, get
|
|
63
|
+
|
|
64
|
+
def test_rate_table_is_the_platforms(self):
|
|
65
|
+
result, get = self._run(
|
|
66
|
+
["--to", "gbp", "--from", "usd,cny"], {"success": True, "result": RESULT}
|
|
67
|
+
)
|
|
68
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
69
|
+
get.assert_called_once_with(to="gbp", sources=["usd", "cny"], amount=None)
|
|
70
|
+
self.assertEqual(json.loads(result.output), RESULT)
|
|
71
|
+
|
|
72
|
+
def test_single_amount_is_converted_by_the_platform(self):
|
|
73
|
+
converted = {
|
|
74
|
+
**RESULT,
|
|
75
|
+
"rates": {"CNY": 0.113},
|
|
76
|
+
"from": "CNY",
|
|
77
|
+
"rate": 0.113,
|
|
78
|
+
"amount": 250.0,
|
|
79
|
+
"converted": 28.25,
|
|
80
|
+
}
|
|
81
|
+
result, get = self._run(
|
|
82
|
+
["--to", "gbp", "--from", "cny", "--amount", "250"],
|
|
83
|
+
{"success": True, "result": converted},
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
86
|
+
get.assert_called_once_with(to="gbp", sources=["cny"], amount=250.0)
|
|
87
|
+
self.assertEqual(json.loads(result.output)["converted"], 28.25)
|
|
88
|
+
|
|
89
|
+
def test_amount_with_several_currencies_is_not_sent(self):
|
|
90
|
+
_, get = self._run(
|
|
91
|
+
["--to", "gbp", "--from", "usd,cny", "--amount", "250"],
|
|
92
|
+
{"success": True, "result": RESULT},
|
|
93
|
+
)
|
|
94
|
+
get.assert_called_once_with(to="gbp", sources=["usd", "cny"], amount=None)
|
|
95
|
+
|
|
96
|
+
def test_a_missing_rate_fails_rather_than_returning_null(self):
|
|
97
|
+
missing = {**RESULT, "rates": {"XYZ": None}, "unavailable": ["XYZ"]}
|
|
98
|
+
result, _ = self._run(
|
|
99
|
+
["--to", "gbp", "--from", "xyz"], {"success": True, "result": missing}
|
|
100
|
+
)
|
|
101
|
+
self.assertEqual(result.exit_code, 1, result.output)
|
|
102
|
+
self.assertIn("XYZ->GBP", result.output)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.14"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|