makea-cli 0.2.8__tar.gz → 0.2.10__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.8 → makea_cli-0.2.10}/CHANGELOG.md +23 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/PKG-INFO +6 -6
- {makea_cli-0.2.8 → makea_cli-0.2.10}/README.md +5 -5
- makea_cli-0.2.10/makea_cli/__init__.py +1 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/api_client.py +93 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/analytics/cmd.py +7 -11
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/brand/cmd.py +89 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/config.py +3 -2
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/posthog_client.py +12 -23
- {makea_cli-0.2.8 → makea_cli-0.2.10}/pyproject.toml +1 -1
- {makea_cli-0.2.8 → makea_cli-0.2.10}/tests/golden_manifest.json +181 -10
- makea_cli-0.2.10/tests/test_pm_reads.py +69 -0
- makea_cli-0.2.8/makea_cli/__init__.py +0 -1
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/.gitignore +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/__main__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/broker.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/brand/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/factory/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/factory/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/metrics/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/orders/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/commands/supplier/cmd.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/errors.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/identity.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/main.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/output.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/makea_cli/registry.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/tests/test_broker.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/tests/test_output_contract.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/tests/test_persona_commands.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/tests/test_registry.py +0 -0
- {makea_cli-0.2.8 → makea_cli-0.2.10}/uv.lock +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
- PM reads for a brand (and Beeboo working for one):
|
|
6
|
+
- `designer get tna`: the Time & Action calendar, every pack's arrival with
|
|
7
|
+
how sure it is (done / confirmed / estimated) and its launch day.
|
|
8
|
+
- `designer list milestone --product <id>`: the bulk order's production
|
|
9
|
+
milestones, flattened (step, milestone, status, submitted/approved).
|
|
10
|
+
- `designer list note --product <id> --channel <key> [--since <date>]`: a
|
|
11
|
+
channel's messages without a search keyword, optionally only recent ones.
|
|
12
|
+
- `designer list fit-round --product <id>`: fit rounds and the corrections
|
|
13
|
+
carried into bulk.
|
|
14
|
+
- `designer get costing --style <id>`: one unit of each pack, factory price,
|
|
15
|
+
fees, the brand's freight/duty estimates, target, RRP, margin.
|
|
16
|
+
|
|
17
|
+
## 0.2.9
|
|
18
|
+
|
|
19
|
+
- The analytics commands (`admin get analytics-query`, `active-users`,
|
|
20
|
+
`top-features`, `top-clicks`, `feature-usage`) no longer need a PostHog key.
|
|
21
|
+
They go through the backend's `POST /admin/analytics/query`, which holds the
|
|
22
|
+
key, so an admin login is enough. A local `POSTHOG_API_KEY` +
|
|
23
|
+
`POSTHOG_PROJECT_ID` still switches them to querying PostHog directly.
|
|
24
|
+
Needs the backend with that endpoint.
|
|
25
|
+
|
|
3
26
|
## 0.2.8
|
|
4
27
|
|
|
5
28
|
- `supplier get pack <quote_request_id> --readable`: the pack as a person reads
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: makea-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
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
|
|
@@ -98,18 +98,18 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
98
98
|
| `makea-cli admin download document` | read | `GET /admin/document/download` | save a document file (document id + designer user_id). | `download-document` |
|
|
99
99
|
| `makea-cli admin download product-document` | read | `GET /admin/document/download` | Resolve product via admin list product-order payload, then GET /admin/document/download. | `download-product-document` |
|
|
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
|
-
| `makea-cli admin get active-users` | read | `
|
|
102
|
-
| `makea-cli admin get analytics-query` | read | `
|
|
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
|
+
| `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
103
|
| `makea-cli admin get currency-conversion` | read | `currency-api` | avoids repeated lookups. | `convert-currency` |
|
|
104
|
-
| `makea-cli admin get feature-usage` | read | `
|
|
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` |
|
|
107
107
|
| `makea-cli admin get quote-request` | read | `GET /admin/quote_requests/{quote_request_id}` | one quote request in full. | — |
|
|
108
108
|
| `makea-cli admin get revenue-metrics` | read | `GET /admin/business-metrics/cumulative-payment-and-platform-revenue` | platform-wide payment received and platform revenue (service fee + transaction fee) bucketed over a date range. | `get-cumulative-payment-and-platform-revenue` |
|
|
109
109
|
| `makea-cli admin get supplier-profile-schema` | read | `GET /admin/supplier/profile/schema` | supplier profile v2 schema (section/field taxonomy: sections, fields, options, required rules). | `get-supplier-profile-schema` |
|
|
110
110
|
| `makea-cli admin get tech-spec-extraction` | read | `GET /admin/products/{id}/extract-tech-specifications` | AI-generate specs (not saved). | `extract-ai-tech-spec` |
|
|
111
|
-
| `makea-cli admin get top-clicks` | read | `
|
|
112
|
-
| `makea-cli admin get top-features` | read | `
|
|
111
|
+
| `makea-cli admin get top-clicks` | read | `POST /admin/analytics/query` | which buttons/elements get clicked most. | `analytics-top-clicks` |
|
|
112
|
+
| `makea-cli admin get top-features` | read | `POST /admin/analytics/query` | a quick map of which features get used most. | `analytics-top-features` |
|
|
113
113
|
| `makea-cli admin list library-element` | read | `GET /admin/designer/{user_id}/library_components` | list a designer's tech-pack library (org-fanned). Optional --section scopes to one type. | `library-list` |
|
|
114
114
|
| `makea-cli admin list misc-payment` | read | `GET /admin/products/{id}/misc-payments` | misc payments for one product (admin). | `list-misc-payments-by-product` |
|
|
115
115
|
| `makea-cli admin list product-order` | read | `GET /admin/designer/get_all_product_orders` | designer product (SKU) list. For 大货 use admin list production-order or admin list production-order --product. | `list-product-orders` |
|
|
@@ -86,18 +86,18 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
86
86
|
| `makea-cli admin download document` | read | `GET /admin/document/download` | save a document file (document id + designer user_id). | `download-document` |
|
|
87
87
|
| `makea-cli admin download product-document` | read | `GET /admin/document/download` | Resolve product via admin list product-order payload, then GET /admin/document/download. | `download-product-document` |
|
|
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
|
-
| `makea-cli admin get active-users` | read | `
|
|
90
|
-
| `makea-cli admin get analytics-query` | read | `
|
|
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
|
+
| `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
91
|
| `makea-cli admin get currency-conversion` | read | `currency-api` | avoids repeated lookups. | `convert-currency` |
|
|
92
|
-
| `makea-cli admin get feature-usage` | read | `
|
|
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` |
|
|
95
95
|
| `makea-cli admin get quote-request` | read | `GET /admin/quote_requests/{quote_request_id}` | one quote request in full. | — |
|
|
96
96
|
| `makea-cli admin get revenue-metrics` | read | `GET /admin/business-metrics/cumulative-payment-and-platform-revenue` | platform-wide payment received and platform revenue (service fee + transaction fee) bucketed over a date range. | `get-cumulative-payment-and-platform-revenue` |
|
|
97
97
|
| `makea-cli admin get supplier-profile-schema` | read | `GET /admin/supplier/profile/schema` | supplier profile v2 schema (section/field taxonomy: sections, fields, options, required rules). | `get-supplier-profile-schema` |
|
|
98
98
|
| `makea-cli admin get tech-spec-extraction` | read | `GET /admin/products/{id}/extract-tech-specifications` | AI-generate specs (not saved). | `extract-ai-tech-spec` |
|
|
99
|
-
| `makea-cli admin get top-clicks` | read | `
|
|
100
|
-
| `makea-cli admin get top-features` | read | `
|
|
99
|
+
| `makea-cli admin get top-clicks` | read | `POST /admin/analytics/query` | which buttons/elements get clicked most. | `analytics-top-clicks` |
|
|
100
|
+
| `makea-cli admin get top-features` | read | `POST /admin/analytics/query` | a quick map of which features get used most. | `analytics-top-features` |
|
|
101
101
|
| `makea-cli admin list library-element` | read | `GET /admin/designer/{user_id}/library_components` | list a designer's tech-pack library (org-fanned). Optional --section scopes to one type. | `library-list` |
|
|
102
102
|
| `makea-cli admin list misc-payment` | read | `GET /admin/products/{id}/misc-payments` | misc payments for one product (admin). | `list-misc-payments-by-product` |
|
|
103
103
|
| `makea-cli admin list product-order` | read | `GET /admin/designer/get_all_product_orders` | designer product (SKU) list. For 大货 use admin list production-order or admin list production-order --product. | `list-product-orders` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.10"
|
|
@@ -1359,6 +1359,99 @@ def supplier_list_quote_edit_requests(quote_request_id: str) -> Any:
|
|
|
1359
1359
|
return get_json(f"/supplier/quote_requests/{quote_request_id}/quote_edit_requests")
|
|
1360
1360
|
|
|
1361
1361
|
|
|
1362
|
+
def designer_get_tna() -> Any:
|
|
1363
|
+
"""GET /designer/tna_calendar — every pack's five steps and arrival, with
|
|
1364
|
+
the launch it is measured against when the arrival is confirmed."""
|
|
1365
|
+
return get_json("/designer/tna_calendar")
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def designer_list_milestones(product_id: str) -> Any:
|
|
1369
|
+
"""Production milestones on a pack's bulk orders, compact: what is done,
|
|
1370
|
+
submitted or waiting, never a lateness the factory did not commit to."""
|
|
1371
|
+
raw = get_json(
|
|
1372
|
+
f"/designer/products/{product_id}/production_order_details",
|
|
1373
|
+
params={"type": "PRODUCTION"},
|
|
1374
|
+
)
|
|
1375
|
+
orders = (
|
|
1376
|
+
raw.get("production_orders_details") or raw.get("orders")
|
|
1377
|
+
if isinstance(raw, dict)
|
|
1378
|
+
else raw
|
|
1379
|
+
)
|
|
1380
|
+
out = []
|
|
1381
|
+
for order in orders or []:
|
|
1382
|
+
if not isinstance(order, dict):
|
|
1383
|
+
continue
|
|
1384
|
+
for step in order.get("production_progress") or []:
|
|
1385
|
+
meta = step.get("metadata") or {}
|
|
1386
|
+
for m in meta.get("milestones") or []:
|
|
1387
|
+
if not isinstance(m, dict):
|
|
1388
|
+
continue
|
|
1389
|
+
out.append(
|
|
1390
|
+
{
|
|
1391
|
+
"order_id": order.get("production_order_id"),
|
|
1392
|
+
"step": step.get("progress_type"),
|
|
1393
|
+
"milestone": m.get("title"),
|
|
1394
|
+
"status": m.get("status"),
|
|
1395
|
+
"days_from_payment": m.get("time_window"),
|
|
1396
|
+
"submitted_at": m.get("submitted_at"),
|
|
1397
|
+
"approval_result": m.get("approval_result"),
|
|
1398
|
+
"completed_at": m.get("completed_at"),
|
|
1399
|
+
}
|
|
1400
|
+
)
|
|
1401
|
+
return {"product_id": product_id, "milestones": out}
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def designer_list_notes(
|
|
1405
|
+
product_id: str, channel_key: str, *, since: str | None = None
|
|
1406
|
+
) -> Any:
|
|
1407
|
+
"""A channel's messages, oldest first, optionally only those after
|
|
1408
|
+
``since`` (ISO date or datetime): what the factory said this week."""
|
|
1409
|
+
raw = get_json(f"/designer/products/{product_id}/channels/{channel_key}/notes")
|
|
1410
|
+
notes = raw.get("notes") if isinstance(raw, dict) else raw
|
|
1411
|
+
out = []
|
|
1412
|
+
for n in notes or []:
|
|
1413
|
+
if not isinstance(n, dict):
|
|
1414
|
+
continue
|
|
1415
|
+
if since and str(n.get("created_at") or "") < since:
|
|
1416
|
+
continue
|
|
1417
|
+
out.append(
|
|
1418
|
+
{
|
|
1419
|
+
"note_id": n.get("note_id"),
|
|
1420
|
+
"at": n.get("created_at"),
|
|
1421
|
+
"from": n.get("created_by_type"),
|
|
1422
|
+
"supplier": n.get("supplier_name"),
|
|
1423
|
+
"text": n.get("content"),
|
|
1424
|
+
"replies": len(n.get("replies") or []),
|
|
1425
|
+
}
|
|
1426
|
+
)
|
|
1427
|
+
return {"product_id": product_id, "channel": channel_key, "notes": out}
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def designer_list_fit_rounds(product_id: str) -> Any:
|
|
1431
|
+
"""GET /designer/products/{id}/fit_rounds — every fit round on the pack and
|
|
1432
|
+
the corrections carried into bulk."""
|
|
1433
|
+
return get_json(f"/designer/products/{product_id}/fit_rounds")
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def designer_get_costing(style_id: str) -> Any:
|
|
1437
|
+
"""One unit of each pack on a product card: factory price, fees, the
|
|
1438
|
+
brand's freight and duty estimates, target, RRP, margin."""
|
|
1439
|
+
raw = get_json(f"/designer/styles/{style_id}/overview")
|
|
1440
|
+
packs = raw.get("packs") if isinstance(raw, dict) else []
|
|
1441
|
+
return {
|
|
1442
|
+
"style_id": style_id,
|
|
1443
|
+
"packs": [
|
|
1444
|
+
{
|
|
1445
|
+
"product_id": p.get("product_id"),
|
|
1446
|
+
"pack": p.get("pack_label"),
|
|
1447
|
+
"costing": p.get("costing"),
|
|
1448
|
+
}
|
|
1449
|
+
for p in packs or []
|
|
1450
|
+
if isinstance(p, dict)
|
|
1451
|
+
],
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
|
|
1362
1455
|
def designer_list_actions(*, product_id: str | None = None) -> Any:
|
|
1363
1456
|
"""GET /designer/actions — the brand's pending actions, optionally for one product."""
|
|
1364
1457
|
params = {"product_id": product_id} if product_id else None
|
|
@@ -4,7 +4,8 @@ Behavioural metrics (feature usage, clicks, page/feature time, active users)
|
|
|
4
4
|
backed by PostHog HogQL. Distinct from the `metrics` group, which reads the
|
|
5
5
|
Makea backend for financial figures.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Goes through the Makea backend by default (admin login is enough); a local
|
|
8
|
+
POSTHOG_API_KEY + POSTHOG_PROJECT_ID switches to querying PostHog directly.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
@@ -32,8 +33,7 @@ def register(app: typer.Typer) -> None:
|
|
|
32
33
|
"get",
|
|
33
34
|
"feature-usage",
|
|
34
35
|
legacy="analytics-feature-usage",
|
|
35
|
-
api="
|
|
36
|
-
external="posthog",
|
|
36
|
+
api="POST /admin/analytics/query",
|
|
37
37
|
help=(
|
|
38
38
|
"How many distinct users used a feature recently, plus event volume. "
|
|
39
39
|
"Matches $pageview path and feature_time_spent events. "
|
|
@@ -82,8 +82,7 @@ def register(app: typer.Typer) -> None:
|
|
|
82
82
|
"get",
|
|
83
83
|
"top-features",
|
|
84
84
|
legacy="analytics-top-features",
|
|
85
|
-
api="
|
|
86
|
-
external="posthog",
|
|
85
|
+
api="POST /admin/analytics/query",
|
|
87
86
|
help=(
|
|
88
87
|
"Rank pageview paths by distinct users over the last N days — a quick "
|
|
89
88
|
"map of which features get used most."
|
|
@@ -118,8 +117,7 @@ def register(app: typer.Typer) -> None:
|
|
|
118
117
|
"get",
|
|
119
118
|
"top-clicks",
|
|
120
119
|
legacy="analytics-top-clicks",
|
|
121
|
-
api="
|
|
122
|
-
external="posthog",
|
|
120
|
+
api="POST /admin/analytics/query",
|
|
123
121
|
help=(
|
|
124
122
|
"Rank autocaptured click targets by count over the last N days — "
|
|
125
123
|
"which buttons/elements get clicked most."
|
|
@@ -156,8 +154,7 @@ def register(app: typer.Typer) -> None:
|
|
|
156
154
|
"get",
|
|
157
155
|
"active-users",
|
|
158
156
|
legacy="analytics-active-users",
|
|
159
|
-
api="
|
|
160
|
-
external="posthog",
|
|
157
|
+
api="POST /admin/analytics/query",
|
|
161
158
|
help="Distinct active users per day over the last N days (DAU series).",
|
|
162
159
|
)
|
|
163
160
|
def active_users(
|
|
@@ -184,8 +181,7 @@ def register(app: typer.Typer) -> None:
|
|
|
184
181
|
"get",
|
|
185
182
|
"analytics-query",
|
|
186
183
|
legacy="analytics-query",
|
|
187
|
-
api="
|
|
188
|
-
external="posthog",
|
|
184
|
+
api="POST /admin/analytics/query",
|
|
189
185
|
help=(
|
|
190
186
|
"Run an arbitrary HogQL query (escape hatch). "
|
|
191
187
|
'Example: analytics-query "SELECT count() FROM events"'
|
|
@@ -212,6 +212,95 @@ def register(app: typer.Typer) -> None:
|
|
|
212
212
|
"""List a product's channels."""
|
|
213
213
|
print_json_or_exit(console, lambda: api_client.designer_list_channels(product))
|
|
214
214
|
|
|
215
|
+
@command(
|
|
216
|
+
app,
|
|
217
|
+
"designer",
|
|
218
|
+
"get",
|
|
219
|
+
"tna",
|
|
220
|
+
api="GET /designer/tna_calendar",
|
|
221
|
+
help=(
|
|
222
|
+
"GET /designer/tna_calendar — when each pack arrives and how sure that is "
|
|
223
|
+
"(done / confirmed / estimated), with its launch day."
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
def get_tna() -> None:
|
|
227
|
+
"""The Time & Action calendar."""
|
|
228
|
+
print_json_or_exit(console, lambda: api_client.designer_get_tna())
|
|
229
|
+
|
|
230
|
+
@command(
|
|
231
|
+
app,
|
|
232
|
+
"designer",
|
|
233
|
+
"list",
|
|
234
|
+
"milestone",
|
|
235
|
+
api="GET /designer/products/{product_id}/production_order_details",
|
|
236
|
+
help=(
|
|
237
|
+
"GET /designer/products/{id}/production_order_details — the bulk order's "
|
|
238
|
+
"production milestones: done, submitted, waiting."
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
def list_milestones(
|
|
242
|
+
product: str = typer.Option(..., "--product", "-p", help="Product id."),
|
|
243
|
+
) -> None:
|
|
244
|
+
"""List a pack's production milestones."""
|
|
245
|
+
print_json_or_exit(console, lambda: api_client.designer_list_milestones(product))
|
|
246
|
+
|
|
247
|
+
@command(
|
|
248
|
+
app,
|
|
249
|
+
"designer",
|
|
250
|
+
"list",
|
|
251
|
+
"note",
|
|
252
|
+
api="GET /designer/products/{product_id}/channels/{channel_key}/notes",
|
|
253
|
+
help=(
|
|
254
|
+
"GET /designer/products/{id}/channels/{key}/notes — a channel's messages, "
|
|
255
|
+
"oldest first; --since keeps the recent ones (no keyword needed)."
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
def list_notes(
|
|
259
|
+
product: str = typer.Option(..., "--product", "-p", help="Product id."),
|
|
260
|
+
channel: str = typer.Option(..., "--channel", "-c", help="Channel key."),
|
|
261
|
+
since: str | None = typer.Option(
|
|
262
|
+
None, "--since", help="ISO date or datetime, e.g. 2026-10-01."
|
|
263
|
+
),
|
|
264
|
+
) -> None:
|
|
265
|
+
"""List a channel's messages."""
|
|
266
|
+
print_json_or_exit(
|
|
267
|
+
console, lambda: api_client.designer_list_notes(product, channel, since=since)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@command(
|
|
271
|
+
app,
|
|
272
|
+
"designer",
|
|
273
|
+
"list",
|
|
274
|
+
"fit-round",
|
|
275
|
+
api="GET /designer/products/{product_id}/fit_rounds",
|
|
276
|
+
help=(
|
|
277
|
+
"GET /designer/products/{id}/fit_rounds — the pack's fit rounds and the "
|
|
278
|
+
"corrections carried into bulk."
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
def list_fit_rounds(
|
|
282
|
+
product: str = typer.Option(..., "--product", "-p", help="Product id."),
|
|
283
|
+
) -> None:
|
|
284
|
+
"""List a pack's fit rounds."""
|
|
285
|
+
print_json_or_exit(console, lambda: api_client.designer_list_fit_rounds(product))
|
|
286
|
+
|
|
287
|
+
@command(
|
|
288
|
+
app,
|
|
289
|
+
"designer",
|
|
290
|
+
"get",
|
|
291
|
+
"costing",
|
|
292
|
+
api="GET /designer/styles/{style_id}/overview",
|
|
293
|
+
help=(
|
|
294
|
+
"GET /designer/styles/{id}/overview — what one unit of each pack costs and "
|
|
295
|
+
"the margin at RRP (freight and duty are the brand's estimates)."
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
def get_costing(
|
|
299
|
+
style: str = typer.Option(..., "--style", "-s", help="Product card (style) id."),
|
|
300
|
+
) -> None:
|
|
301
|
+
"""A product card's unit cost and margin."""
|
|
302
|
+
print_json_or_exit(console, lambda: api_client.designer_get_costing(style))
|
|
303
|
+
|
|
215
304
|
@command(
|
|
216
305
|
app,
|
|
217
306
|
"designer",
|
|
@@ -48,8 +48,9 @@ def redirect_uri() -> str:
|
|
|
48
48
|
|
|
49
49
|
# PostHog product-analytics (behavioural metrics: clicks, page/feature time,
|
|
50
50
|
# funnels, retention). Distinct from the financial `metrics` command group,
|
|
51
|
-
# which reads the Makea backend.
|
|
52
|
-
#
|
|
51
|
+
# which reads the Makea backend. Optional: without these the analytics commands
|
|
52
|
+
# go through the backend's /admin/analytics/query, which holds the key. Set them
|
|
53
|
+
# only to query PostHog directly:
|
|
53
54
|
# POSTHOG_API_KEY personal API key (phx_...), read-only is enough
|
|
54
55
|
# POSTHOG_PROJECT_ID numeric project id
|
|
55
56
|
# POSTHOG_HOST defaults to the EU cloud query host
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Minimal PostHog query client (HogQL over the Query API).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
own auth (a personal API key from env) rather than the Cognito token used by
|
|
5
|
-
api_client.
|
|
3
|
+
Two routes to the same query:
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
- By default it goes through the Makea backend (POST /admin/analytics/query),
|
|
6
|
+
which holds the PostHog key. An admin's `makea-cli login` is all it takes.
|
|
7
|
+
- If POSTHOG_API_KEY and POSTHOG_PROJECT_ID are set locally, it talks to
|
|
8
|
+
PostHog directly instead (POST {host}/api/projects/{project_id}/query/).
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
@@ -14,33 +15,21 @@ from typing import Any
|
|
|
14
15
|
|
|
15
16
|
import httpx
|
|
16
17
|
|
|
18
|
+
from makea_cli import api_client
|
|
17
19
|
from makea_cli.config import posthog_api_key, posthog_host, posthog_project_id
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
"""Raised when the required PostHog env vars are missing."""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _require_config() -> tuple[str, str, str]:
|
|
25
|
-
key = posthog_api_key()
|
|
26
|
-
project = posthog_project_id()
|
|
27
|
-
if not key or not project:
|
|
28
|
-
raise PostHogNotConfigured(
|
|
29
|
-
"PostHog is not configured. Set POSTHOG_API_KEY (personal API key, "
|
|
30
|
-
"phx_...) and POSTHOG_PROJECT_ID, optionally POSTHOG_HOST "
|
|
31
|
-
"(default https://eu.posthog.com)."
|
|
32
|
-
)
|
|
33
|
-
return posthog_host(), project, key
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def run_hogql(query: str, *, timeout: float = 60.0) -> dict[str, Any]:
|
|
22
|
+
def run_hogql(query: str, *, timeout: float = 90.0) -> dict[str, Any]:
|
|
37
23
|
"""Run a HogQL query and return the raw result.
|
|
38
24
|
|
|
39
25
|
The result has ``columns`` (list of names) and ``results`` (list of rows);
|
|
40
26
|
callers usually want to zip them into dicts for display.
|
|
41
27
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
key = posthog_api_key()
|
|
29
|
+
project = posthog_project_id()
|
|
30
|
+
if not key or not project:
|
|
31
|
+
return api_client.post_json("/admin/analytics/query", {"query": query}, timeout=timeout)
|
|
32
|
+
url = f"{posthog_host()}/api/projects/{project}/query/"
|
|
44
33
|
headers = {
|
|
45
34
|
"Authorization": f"Bearer {key}",
|
|
46
35
|
"Content-Type": "application/json",
|
|
@@ -913,8 +913,8 @@
|
|
|
913
913
|
"verb": "get",
|
|
914
914
|
"noun": "active-users",
|
|
915
915
|
"access": "read",
|
|
916
|
-
"api": "
|
|
917
|
-
"external":
|
|
916
|
+
"api": "POST /admin/analytics/query",
|
|
917
|
+
"external": null,
|
|
918
918
|
"side_effects": [],
|
|
919
919
|
"approval": "auto",
|
|
920
920
|
"reversible": null,
|
|
@@ -947,8 +947,8 @@
|
|
|
947
947
|
"verb": "get",
|
|
948
948
|
"noun": "analytics-query",
|
|
949
949
|
"access": "read",
|
|
950
|
-
"api": "
|
|
951
|
-
"external":
|
|
950
|
+
"api": "POST /admin/analytics/query",
|
|
951
|
+
"external": null,
|
|
952
952
|
"side_effects": [],
|
|
953
953
|
"approval": "auto",
|
|
954
954
|
"reversible": null,
|
|
@@ -1040,8 +1040,8 @@
|
|
|
1040
1040
|
"verb": "get",
|
|
1041
1041
|
"noun": "feature-usage",
|
|
1042
1042
|
"access": "read",
|
|
1043
|
-
"api": "
|
|
1044
|
-
"external":
|
|
1043
|
+
"api": "POST /admin/analytics/query",
|
|
1044
|
+
"external": null,
|
|
1045
1045
|
"side_effects": [],
|
|
1046
1046
|
"approval": "auto",
|
|
1047
1047
|
"reversible": null,
|
|
@@ -1347,8 +1347,8 @@
|
|
|
1347
1347
|
"verb": "get",
|
|
1348
1348
|
"noun": "top-clicks",
|
|
1349
1349
|
"access": "read",
|
|
1350
|
-
"api": "
|
|
1351
|
-
"external":
|
|
1350
|
+
"api": "POST /admin/analytics/query",
|
|
1351
|
+
"external": null,
|
|
1352
1352
|
"side_effects": [],
|
|
1353
1353
|
"approval": "auto",
|
|
1354
1354
|
"reversible": null,
|
|
@@ -1394,8 +1394,8 @@
|
|
|
1394
1394
|
"verb": "get",
|
|
1395
1395
|
"noun": "top-features",
|
|
1396
1396
|
"access": "read",
|
|
1397
|
-
"api": "
|
|
1398
|
-
"external":
|
|
1397
|
+
"api": "POST /admin/analytics/query",
|
|
1398
|
+
"external": null,
|
|
1399
1399
|
"side_effects": [],
|
|
1400
1400
|
"approval": "auto",
|
|
1401
1401
|
"reversible": null,
|
|
@@ -5257,6 +5257,38 @@
|
|
|
5257
5257
|
}
|
|
5258
5258
|
]
|
|
5259
5259
|
},
|
|
5260
|
+
{
|
|
5261
|
+
"name": "designer get costing",
|
|
5262
|
+
"persona": "designer",
|
|
5263
|
+
"verb": "get",
|
|
5264
|
+
"noun": "costing",
|
|
5265
|
+
"access": "read",
|
|
5266
|
+
"api": "GET /designer/styles/{style_id}/overview",
|
|
5267
|
+
"external": null,
|
|
5268
|
+
"side_effects": [],
|
|
5269
|
+
"approval": "auto",
|
|
5270
|
+
"reversible": null,
|
|
5271
|
+
"idempotent": null,
|
|
5272
|
+
"limit": null,
|
|
5273
|
+
"legacy": [],
|
|
5274
|
+
"help": "GET /designer/styles/{id}/overview — what one unit of each pack costs and the margin at RRP (freight and duty are the brand's estimates).",
|
|
5275
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.get_costing",
|
|
5276
|
+
"flags": [
|
|
5277
|
+
{
|
|
5278
|
+
"name": "style",
|
|
5279
|
+
"kind": "option",
|
|
5280
|
+
"opts": [
|
|
5281
|
+
"--style",
|
|
5282
|
+
"-s"
|
|
5283
|
+
],
|
|
5284
|
+
"type": "text",
|
|
5285
|
+
"required": true,
|
|
5286
|
+
"multiple": false,
|
|
5287
|
+
"default": null,
|
|
5288
|
+
"help": "Product card (style) id."
|
|
5289
|
+
}
|
|
5290
|
+
]
|
|
5291
|
+
},
|
|
5260
5292
|
{
|
|
5261
5293
|
"name": "designer get lifecycle",
|
|
5262
5294
|
"persona": "designer",
|
|
@@ -5468,6 +5500,24 @@
|
|
|
5468
5500
|
}
|
|
5469
5501
|
]
|
|
5470
5502
|
},
|
|
5503
|
+
{
|
|
5504
|
+
"name": "designer get tna",
|
|
5505
|
+
"persona": "designer",
|
|
5506
|
+
"verb": "get",
|
|
5507
|
+
"noun": "tna",
|
|
5508
|
+
"access": "read",
|
|
5509
|
+
"api": "GET /designer/tna_calendar",
|
|
5510
|
+
"external": null,
|
|
5511
|
+
"side_effects": [],
|
|
5512
|
+
"approval": "auto",
|
|
5513
|
+
"reversible": null,
|
|
5514
|
+
"idempotent": null,
|
|
5515
|
+
"limit": null,
|
|
5516
|
+
"legacy": [],
|
|
5517
|
+
"help": "GET /designer/tna_calendar — when each pack arrives and how sure that is (done / confirmed / estimated), with its launch day.",
|
|
5518
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.get_tna",
|
|
5519
|
+
"flags": []
|
|
5520
|
+
},
|
|
5471
5521
|
{
|
|
5472
5522
|
"name": "designer list action",
|
|
5473
5523
|
"persona": "designer",
|
|
@@ -5550,6 +5600,38 @@
|
|
|
5550
5600
|
"function": "makea_cli.commands.brand.cmd.register.<locals>.list_collections",
|
|
5551
5601
|
"flags": []
|
|
5552
5602
|
},
|
|
5603
|
+
{
|
|
5604
|
+
"name": "designer list fit-round",
|
|
5605
|
+
"persona": "designer",
|
|
5606
|
+
"verb": "list",
|
|
5607
|
+
"noun": "fit-round",
|
|
5608
|
+
"access": "read",
|
|
5609
|
+
"api": "GET /designer/products/{product_id}/fit_rounds",
|
|
5610
|
+
"external": null,
|
|
5611
|
+
"side_effects": [],
|
|
5612
|
+
"approval": "auto",
|
|
5613
|
+
"reversible": null,
|
|
5614
|
+
"idempotent": null,
|
|
5615
|
+
"limit": "client",
|
|
5616
|
+
"legacy": [],
|
|
5617
|
+
"help": "GET /designer/products/{id}/fit_rounds — the pack's fit rounds and the corrections carried into bulk.",
|
|
5618
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.list_fit_rounds",
|
|
5619
|
+
"flags": [
|
|
5620
|
+
{
|
|
5621
|
+
"name": "product",
|
|
5622
|
+
"kind": "option",
|
|
5623
|
+
"opts": [
|
|
5624
|
+
"--product",
|
|
5625
|
+
"-p"
|
|
5626
|
+
],
|
|
5627
|
+
"type": "text",
|
|
5628
|
+
"required": true,
|
|
5629
|
+
"multiple": false,
|
|
5630
|
+
"default": null,
|
|
5631
|
+
"help": "Product id."
|
|
5632
|
+
}
|
|
5633
|
+
]
|
|
5634
|
+
},
|
|
5553
5635
|
{
|
|
5554
5636
|
"name": "designer list library-element",
|
|
5555
5637
|
"persona": "designer",
|
|
@@ -5593,6 +5675,95 @@
|
|
|
5593
5675
|
}
|
|
5594
5676
|
]
|
|
5595
5677
|
},
|
|
5678
|
+
{
|
|
5679
|
+
"name": "designer list milestone",
|
|
5680
|
+
"persona": "designer",
|
|
5681
|
+
"verb": "list",
|
|
5682
|
+
"noun": "milestone",
|
|
5683
|
+
"access": "read",
|
|
5684
|
+
"api": "GET /designer/products/{product_id}/production_order_details",
|
|
5685
|
+
"external": null,
|
|
5686
|
+
"side_effects": [],
|
|
5687
|
+
"approval": "auto",
|
|
5688
|
+
"reversible": null,
|
|
5689
|
+
"idempotent": null,
|
|
5690
|
+
"limit": "client",
|
|
5691
|
+
"legacy": [],
|
|
5692
|
+
"help": "GET /designer/products/{id}/production_order_details — the bulk order's production milestones: done, submitted, waiting.",
|
|
5693
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.list_milestones",
|
|
5694
|
+
"flags": [
|
|
5695
|
+
{
|
|
5696
|
+
"name": "product",
|
|
5697
|
+
"kind": "option",
|
|
5698
|
+
"opts": [
|
|
5699
|
+
"--product",
|
|
5700
|
+
"-p"
|
|
5701
|
+
],
|
|
5702
|
+
"type": "text",
|
|
5703
|
+
"required": true,
|
|
5704
|
+
"multiple": false,
|
|
5705
|
+
"default": null,
|
|
5706
|
+
"help": "Product id."
|
|
5707
|
+
}
|
|
5708
|
+
]
|
|
5709
|
+
},
|
|
5710
|
+
{
|
|
5711
|
+
"name": "designer list note",
|
|
5712
|
+
"persona": "designer",
|
|
5713
|
+
"verb": "list",
|
|
5714
|
+
"noun": "note",
|
|
5715
|
+
"access": "read",
|
|
5716
|
+
"api": "GET /designer/products/{product_id}/channels/{channel_key}/notes",
|
|
5717
|
+
"external": null,
|
|
5718
|
+
"side_effects": [],
|
|
5719
|
+
"approval": "auto",
|
|
5720
|
+
"reversible": null,
|
|
5721
|
+
"idempotent": null,
|
|
5722
|
+
"limit": "client",
|
|
5723
|
+
"legacy": [],
|
|
5724
|
+
"help": "GET /designer/products/{id}/channels/{key}/notes — a channel's messages, oldest first; --since keeps the recent ones (no keyword needed).",
|
|
5725
|
+
"function": "makea_cli.commands.brand.cmd.register.<locals>.list_notes",
|
|
5726
|
+
"flags": [
|
|
5727
|
+
{
|
|
5728
|
+
"name": "product",
|
|
5729
|
+
"kind": "option",
|
|
5730
|
+
"opts": [
|
|
5731
|
+
"--product",
|
|
5732
|
+
"-p"
|
|
5733
|
+
],
|
|
5734
|
+
"type": "text",
|
|
5735
|
+
"required": true,
|
|
5736
|
+
"multiple": false,
|
|
5737
|
+
"default": null,
|
|
5738
|
+
"help": "Product id."
|
|
5739
|
+
},
|
|
5740
|
+
{
|
|
5741
|
+
"name": "channel",
|
|
5742
|
+
"kind": "option",
|
|
5743
|
+
"opts": [
|
|
5744
|
+
"--channel",
|
|
5745
|
+
"-c"
|
|
5746
|
+
],
|
|
5747
|
+
"type": "text",
|
|
5748
|
+
"required": true,
|
|
5749
|
+
"multiple": false,
|
|
5750
|
+
"default": null,
|
|
5751
|
+
"help": "Channel key."
|
|
5752
|
+
},
|
|
5753
|
+
{
|
|
5754
|
+
"name": "since",
|
|
5755
|
+
"kind": "option",
|
|
5756
|
+
"opts": [
|
|
5757
|
+
"--since"
|
|
5758
|
+
],
|
|
5759
|
+
"type": "text",
|
|
5760
|
+
"required": false,
|
|
5761
|
+
"multiple": false,
|
|
5762
|
+
"default": null,
|
|
5763
|
+
"help": "ISO date or datetime, e.g. 2026-10-01."
|
|
5764
|
+
}
|
|
5765
|
+
]
|
|
5766
|
+
},
|
|
5596
5767
|
{
|
|
5597
5768
|
"name": "designer list payment",
|
|
5598
5769
|
"persona": "designer",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""The PM reads Beeboo uses: milestones, recent notes, costing — trimmed to
|
|
2
|
+
what answers "will it be on time", "what did the factory say this week",
|
|
3
|
+
"what does it cost".
|
|
4
|
+
|
|
5
|
+
Run with ``.venv/bin/python -m unittest tests.test_pm_reads``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
from makea_cli import api_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PmReadsTest(unittest.TestCase):
|
|
15
|
+
def test_milestones_are_flattened(self):
|
|
16
|
+
raw = {
|
|
17
|
+
"production_orders_details": [
|
|
18
|
+
{
|
|
19
|
+
"production_order_id": "po1",
|
|
20
|
+
"production_progress": [
|
|
21
|
+
{
|
|
22
|
+
"progress_type": "During Production",
|
|
23
|
+
"metadata": {
|
|
24
|
+
"milestones": [
|
|
25
|
+
{"title": "Cutting", "status": "completed", "time_window": ["3", "5"]},
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
with mock.patch.object(api_client, "get_json", return_value=raw):
|
|
34
|
+
out = api_client.designer_list_milestones("p1")
|
|
35
|
+
self.assertEqual(
|
|
36
|
+
out["milestones"][0],
|
|
37
|
+
{
|
|
38
|
+
"order_id": "po1",
|
|
39
|
+
"step": "During Production",
|
|
40
|
+
"milestone": "Cutting",
|
|
41
|
+
"status": "completed",
|
|
42
|
+
"days_from_payment": ["3", "5"],
|
|
43
|
+
"submitted_at": None,
|
|
44
|
+
"approval_result": None,
|
|
45
|
+
"completed_at": None,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def test_notes_since_keep_the_recent_ones(self):
|
|
50
|
+
raw = {
|
|
51
|
+
"notes": [
|
|
52
|
+
{"note_id": "a", "created_at": "2026-09-30T10:00:00", "content": "old"},
|
|
53
|
+
{"note_id": "b", "created_at": "2026-10-05T10:00:00", "content": "new", "replies": [{}]},
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
with mock.patch.object(api_client, "get_json", return_value=raw):
|
|
57
|
+
out = api_client.designer_list_notes("p1", "supplier:s", since="2026-10-01")
|
|
58
|
+
self.assertEqual([n["note_id"] for n in out["notes"]], ["b"])
|
|
59
|
+
self.assertEqual(out["notes"][0]["replies"], 1)
|
|
60
|
+
|
|
61
|
+
def test_costing_is_per_pack(self):
|
|
62
|
+
raw = {"packs": [{"product_id": "p1", "pack_label": "v1", "costing": {"make": 50}}]}
|
|
63
|
+
with mock.patch.object(api_client, "get_json", return_value=raw):
|
|
64
|
+
out = api_client.designer_get_costing("st1")
|
|
65
|
+
self.assertEqual(out["packs"], [{"product_id": "p1", "pack": "v1", "costing": {"make": 50}}])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.8"
|
|
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
|