makea-cli 0.2.24__py3-none-any.whl → 0.2.26__py3-none-any.whl

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/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.24"
1
+ __version__ = "0.2.26"
makea_cli/api_client.py CHANGED
@@ -1282,6 +1282,131 @@ def admin_playbook_preview(
1282
1282
  return get_json("/admin/playbook/preview", params=params)
1283
1283
 
1284
1284
 
1285
+ # --- admin: what the admin page does by hand (orders, products, people, flags) -----
1286
+ # None of these move money: order statuses that record a payment are refused by
1287
+ # the commands, and change_step refuses money steps server-side.
1288
+
1289
+
1290
+ def admin_get_supplier(supplier_id: str) -> Any:
1291
+ """GET /admin/supplier/get_supplier_by_id?supplier_id= — profile plus financial documents."""
1292
+ return get_json("/admin/supplier/get_supplier_by_id", params={"supplier_id": supplier_id})
1293
+
1294
+
1295
+ def admin_get_user_profile(user_id: str) -> Any:
1296
+ """GET /admin/user-profiles/{user_id}"""
1297
+ return get_json(f"/admin/user-profiles/{quote(user_id, safe='')}")
1298
+
1299
+
1300
+ def admin_update_sampling_order_status(sampling_order_id: str, status: str) -> Any:
1301
+ """PUT /admin/sampling_orders/{id}/status"""
1302
+ return put_json(f"/admin/sampling_orders/{sampling_order_id}/status", {"status": status})
1303
+
1304
+
1305
+ def admin_update_production_order_status(production_order_id: str, status: str) -> Any:
1306
+ """PUT /admin/production_orders/{id}/status"""
1307
+ return put_json(f"/admin/production_orders/{production_order_id}/status", {"status": status})
1308
+
1309
+
1310
+ def admin_set_sampling_order_tracking(sampling_order_id: str, body: dict[str, Any]) -> Any:
1311
+ """PUT /admin/sampling_orders/{id}/tracking — tracking_number / tracking_url /
1312
+ shipping_method / carrier_id, flat in the body."""
1313
+ return put_json(f"/admin/sampling_orders/{sampling_order_id}/tracking", body)
1314
+
1315
+
1316
+ def admin_set_production_order_tracking(production_order_id: str, body: dict[str, Any]) -> Any:
1317
+ """POST /admin/production_orders/{id}/tracking — as sampling, plus cartons and
1318
+ gross_weight_kg (absent = unchanged, empty string = cleared)."""
1319
+ return post_json(f"/admin/production_orders/{production_order_id}/tracking", body)
1320
+
1321
+
1322
+ def admin_get_product_status(product_id: str) -> Any:
1323
+ """GET /admin/products/{id}/status — the product's stage · step · owner per supplier,
1324
+ with the step catalog."""
1325
+ return get_json(f"/admin/products/{product_id}/status")
1326
+
1327
+
1328
+ def admin_get_change_step(product_id: str, supplier_id: str) -> Any:
1329
+ """GET /admin/products/{id}/suppliers/{sid}/change_step — where the factory is
1330
+ and which steps can be picked."""
1331
+ return get_json(f"/admin/products/{product_id}/suppliers/{supplier_id}/change_step")
1332
+
1333
+
1334
+ def admin_change_step(product_id: str, supplier_id: str, body: dict[str, Any]) -> Any:
1335
+ """POST /admin/products/{id}/suppliers/{sid}/change_step —
1336
+ {stage, step, note?, notify_brand?, notify_supplier?}."""
1337
+ return post_json(f"/admin/products/{product_id}/suppliers/{supplier_id}/change_step", body)
1338
+
1339
+
1340
+ def admin_list_product_notes(product_id: str) -> Any:
1341
+ """GET /admin/products/{id}/notes — the product's customer notes."""
1342
+ return get_json(f"/admin/products/{product_id}/notes")
1343
+
1344
+
1345
+ def admin_add_product_note(product_id: str, content: str, *, internal: bool) -> Any:
1346
+ """POST /admin/products/{id}/notes — {content, is_internal}. The backend reads
1347
+ ``is_internal``; a customer note (the default) is emailed to the brand."""
1348
+ return post_json(
1349
+ f"/admin/products/{product_id}/notes",
1350
+ {"content": content, "is_internal": internal},
1351
+ )
1352
+
1353
+
1354
+ def admin_update_product_basic_info(product_id: str, body: dict[str, Any]) -> Any:
1355
+ """PUT /admin/products/{id}/basic-info — only the fields given are changed."""
1356
+ return put_json(f"/admin/products/{product_id}/basic-info", body)
1357
+
1358
+
1359
+ def admin_update_product_document_status(product_id: str, document_id: str, status: str) -> Any:
1360
+ """POST /admin/designer/update_document_status — {product_id, document_id, status}."""
1361
+ return post_json(
1362
+ "/admin/designer/update_document_status",
1363
+ {"product_id": product_id, "document_id": document_id, "status": status},
1364
+ )
1365
+
1366
+
1367
+ def admin_link_supplier(body: dict[str, Any]) -> Any:
1368
+ """POST /admin/product/link_supplier — a bare assignment (no price, so not a quote)."""
1369
+ return post_json("/admin/product/link_supplier", body)
1370
+
1371
+
1372
+ def admin_unlink_supplier(
1373
+ product_id: str, *, link_id: str | None = None, supplier_id: str | None = None
1374
+ ) -> Any:
1375
+ """POST /admin/product/unlink_supplier?product_id=&link_id=|supplier_id= (query, no body)."""
1376
+ params: dict[str, Any] = {"product_id": product_id}
1377
+ if link_id:
1378
+ params["link_id"] = link_id
1379
+ if supplier_id:
1380
+ params["supplier_id"] = supplier_id
1381
+ return _request("POST", "/admin/product/unlink_supplier", params=params)
1382
+
1383
+
1384
+ def admin_reply_quote_request(quote_request_id: str, body: dict[str, Any]) -> Any:
1385
+ """POST /admin/quote_requests/{id}/reply_status_note — {status_note, notification_kind}."""
1386
+ return post_json(f"/admin/quote_requests/{quote_request_id}/reply_status_note", body)
1387
+
1388
+
1389
+ def admin_list_feature_flags() -> Any:
1390
+ """GET /admin/feature-flags — every flag with its allow-list resolved."""
1391
+ return get_json("/admin/feature-flags")
1392
+
1393
+
1394
+ def admin_create_feature_flag(body: dict[str, Any]) -> Any:
1395
+ """POST /admin/feature-flags — {key, description?, enabled?, value?}."""
1396
+ return post_json("/admin/feature-flags", body)
1397
+
1398
+
1399
+ def admin_update_feature_flag(key: str, body: dict[str, Any]) -> Any:
1400
+ """PATCH /admin/feature-flags/{key} — enabled / description / value /
1401
+ add_user_id / remove_user_id, one account at a time."""
1402
+ return patch_json(f"/admin/feature-flags/{quote(key, safe='')}", body)
1403
+
1404
+
1405
+ def admin_delete_feature_flag(key: str) -> Any:
1406
+ """DELETE /admin/feature-flags/{key} — every check of the flag then reads false."""
1407
+ return delete_json(f"/admin/feature-flags/{quote(key, safe='')}")
1408
+
1409
+
1285
1410
  # --- supplier persona: a factory acting on its own RFQs, quotes and orders --------
1286
1411
 
1287
1412
 
@@ -1734,6 +1859,13 @@ def designer_approve_proposal(note_id: str, proposal_id: str) -> Any:
1734
1859
  )
1735
1860
 
1736
1861
 
1862
+ def designer_create_proposals(product_id: str, proposals: list[dict[str, Any]]) -> Any:
1863
+ """POST /designer/products/{id}/tech_spec/proposals — change cards on the tech pack."""
1864
+ return post_json(
1865
+ f"/designer/products/{product_id}/tech_spec/proposals", {"proposals": proposals}
1866
+ )
1867
+
1868
+
1737
1869
  def designer_bulk_accept_actions(channel_id: str, actions: list[dict[str, str]]) -> Any:
1738
1870
  """POST /designer/channels/{id}/suggested_actions/bulk_accept — accept several at once."""
1739
1871
  return post_json(
@@ -11,6 +11,7 @@ from makea_cli.commands.directory.cmd import register as register_directory
11
11
  from makea_cli.commands.document.cmd import register as register_document
12
12
  from makea_cli.commands.email.cmd import register as register_email
13
13
  from makea_cli.commands.factory.cmd import register as register_factory
14
+ from makea_cli.commands.flags.cmd import register as register_flags
14
15
  from makea_cli.commands.library.cmd import register as register_library
15
16
  from makea_cli.commands.metrics.cmd import register as register_metrics
16
17
  from makea_cli.commands.orders.cmd import register as register_orders
@@ -41,3 +42,4 @@ def register_all(app: typer.Typer) -> None:
41
42
  register_playbook(app)
42
43
  register_about(app)
43
44
  register_tasks(app)
45
+ register_flags(app)
@@ -666,6 +666,90 @@ def register(app: typer.Typer) -> None:
666
666
  """Approve one assistant proposal."""
667
667
  print_json_or_exit(console, lambda: api_client.designer_approve_proposal(note, proposal_id))
668
668
 
669
+ @command(
670
+ app,
671
+ "designer",
672
+ "create",
673
+ "proposal",
674
+ api="POST /designer/products/{product_id}/tech_spec/proposals",
675
+ side_effects=[
676
+ "puts a change card on the tech pack; the pack changes only when the user applies it"
677
+ ],
678
+ approval="auto",
679
+ help=(
680
+ "POST /designer/products/{id}/tech_spec/proposals — propose a tech-pack change as a "
681
+ "card on the field it changes. Name the section and field as `get tech-pack "
682
+ "--readable` shows them; pick a row with --item (a value it holds) or --entry, a line "
683
+ "with --was. A refusal lists the sections, fields, rows or lines that exist. "
684
+ "--file takes a JSON list of the same keys for several changes at once."
685
+ ),
686
+ )
687
+ def create_proposal(
688
+ product: str = typer.Option(..., "--product", "-p", help="Product id."),
689
+ section: str | None = typer.Option(
690
+ None, "--section", help='Section, by label or key (e.g. "Trims & hardware").'
691
+ ),
692
+ field: str | None = typer.Option(
693
+ None, "--field", help="Field, by label or key (field-based packs)."
694
+ ),
695
+ value: str | None = typer.Option(None, "--value", help="The new value or line."),
696
+ reason: str | None = typer.Option(
697
+ None, "--reason", help="One line the user reads on the card."
698
+ ),
699
+ action: str = typer.Option(
700
+ "update", "--action", help="update (default), add (a new row or line) or delete."
701
+ ),
702
+ entry: int | None = typer.Option(
703
+ None, "--entry", help="Row number in a repeating section, 0 = first."
704
+ ),
705
+ item: str | None = typer.Option(
706
+ None, "--item", help='Pick the row that holds this value now (e.g. "cord lock").'
707
+ ),
708
+ was: str | None = typer.Option(
709
+ None, "--was", help="Current text of the line to change (line-based packs)."
710
+ ),
711
+ kind: str | None = typer.Option(
712
+ None, "--kind", help="For a new line: spec (default) or note."
713
+ ),
714
+ replaces: list[str] = typer.Option(
715
+ [], "--replaces", help="line_id a new line replaces (repeat)."
716
+ ),
717
+ document: list[str] = typer.Option(
718
+ [], "--document", help="document_id the change comes from (repeat)."
719
+ ),
720
+ file: Path | None = typer.Option(
721
+ None,
722
+ "--file",
723
+ "-f",
724
+ exists=True,
725
+ dir_okay=False,
726
+ help="JSON list of proposals with the keys above, instead of the flags.",
727
+ ),
728
+ ) -> None:
729
+ """Propose tech-pack changes."""
730
+ if file is not None:
731
+ items = json.loads(file.read_text(encoding="utf-8"))
732
+ if not isinstance(items, list):
733
+ raise typer.BadParameter("--file must hold a JSON list of proposals")
734
+ else:
735
+ if not section or not reason:
736
+ raise typer.BadParameter("--section and --reason are required (or --file)")
737
+ one: dict[str, Any] = {
738
+ "section": section,
739
+ "field": field,
740
+ "value": value,
741
+ "reason": reason,
742
+ "action": action,
743
+ "entry": entry,
744
+ "item": item,
745
+ "was": was,
746
+ "kind": kind,
747
+ "replaces": replaces or None,
748
+ "documents": document or None,
749
+ }
750
+ items = [{k: v for k, v in one.items() if v is not None}]
751
+ print_json_or_exit(console, lambda: api_client.designer_create_proposals(product, items))
752
+
669
753
  @command(
670
754
  app,
671
755
  "designer",
@@ -54,6 +54,20 @@ def register(app: typer.Typer) -> None:
54
54
  """
55
55
  print_json_or_exit(console, lambda: api_client.get_all_users(limit=limit, offset=offset))
56
56
 
57
+ @command(
58
+ app,
59
+ "admin",
60
+ "get",
61
+ "user",
62
+ api="GET /admin/user-profiles/{user_id}",
63
+ help="GET /admin/user-profiles/{user_id} — one account's profile (Cognito sub as id).",
64
+ )
65
+ def get_user(
66
+ user_id: str = typer.Argument(..., help="Cognito sub (user_id) of the account."),
67
+ ) -> None:
68
+ """Show one user's profile."""
69
+ print_json_or_exit(console, lambda: api_client.admin_get_user_profile(user_id))
70
+
57
71
  @command(
58
72
  app,
59
73
  "admin",
File without changes
@@ -0,0 +1,157 @@
1
+ """Admin feature flags: the switches on the admin page's Flags tab.
2
+
3
+ A flag is on globally, or, while the global switch is off, on for the accounts
4
+ on its allow-list. The allow-list holds user ids (Cognito sub), never emails.
5
+ Every change reaches prod users at once, so every write asks first.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any
12
+
13
+ import typer
14
+ from rich.console import Console
15
+
16
+ from makea_cli import api_client
17
+ from makea_cli.commands._util import print_json_or_exit
18
+ from makea_cli.registry import command
19
+
20
+ console = Console()
21
+
22
+ # Mirrors garmentby_service.common.feature_flags (FLAG_KEY_PATTERN, MAX_FLAG_KEY_CHARS).
23
+ _FLAG_KEY = re.compile(r"^[A-Z][A-Z0-9_]*$")
24
+ _FLAG_KEY_MAX = 64
25
+
26
+
27
+ def _flag_key(raw: str) -> str:
28
+ key = raw.strip().upper()
29
+ if not _FLAG_KEY.match(key) or len(key) > _FLAG_KEY_MAX:
30
+ raise typer.BadParameter(
31
+ f"flag key must be ALL_CAPS with underscores, at most {_FLAG_KEY_MAX} characters"
32
+ )
33
+ return key
34
+
35
+
36
+ def register(app: typer.Typer) -> None:
37
+ @command(
38
+ app,
39
+ "admin",
40
+ "list",
41
+ "feature-flag",
42
+ api="GET /admin/feature-flags",
43
+ help=(
44
+ "GET /admin/feature-flags — every flag: global switch, default value, and the "
45
+ "accounts it is on for (id, email, name)."
46
+ ),
47
+ )
48
+ def list_feature_flags() -> None:
49
+ """List feature flags."""
50
+ print_json_or_exit(console, api_client.admin_list_feature_flags)
51
+
52
+ @command(
53
+ app,
54
+ "admin",
55
+ "create",
56
+ "feature-flag",
57
+ api="POST /admin/feature-flags",
58
+ side_effects=["with --enabled the flag is on for every account at once"],
59
+ approval="ask",
60
+ reversible=True,
61
+ idempotent=False,
62
+ help=(
63
+ "POST /admin/feature-flags — add a flag (off unless --enabled). An existing key "
64
+ "is refused."
65
+ ),
66
+ )
67
+ def create_feature_flag(
68
+ key: str = typer.Argument(..., help="Flag key, ALL_CAPS_WITH_UNDERSCORES."),
69
+ description: str | None = typer.Option(None, "--description", help="What it gates."),
70
+ enabled: bool = typer.Option(False, "--enabled", help="Turn it on globally now."),
71
+ value: str | None = typer.Option(
72
+ None,
73
+ "--value",
74
+ help="Default value, for flags that name a choice (e.g. DEEPSEEK).",
75
+ ),
76
+ ) -> None:
77
+ """Create a feature flag."""
78
+ body: dict[str, Any] = {"key": _flag_key(key), "enabled": enabled}
79
+ if description is not None:
80
+ body["description"] = description.strip()
81
+ if value is not None:
82
+ body["value"] = value.strip()
83
+ print_json_or_exit(console, lambda: api_client.admin_create_feature_flag(body))
84
+
85
+ @command(
86
+ app,
87
+ "admin",
88
+ "update",
89
+ "feature-flag",
90
+ api="PATCH /admin/feature-flags/{key}",
91
+ side_effects=["the change reaches the affected accounts at once"],
92
+ approval="ask",
93
+ reversible=True,
94
+ idempotent=True,
95
+ help=(
96
+ "PATCH /admin/feature-flags/{key} — flip the global switch, edit the description "
97
+ "or default value, or add / remove one account on the allow-list (by user id; "
98
+ "find it with admin list user)."
99
+ ),
100
+ )
101
+ def update_feature_flag(
102
+ key: str = typer.Argument(..., help="Flag key."),
103
+ enabled: bool | None = typer.Option(
104
+ None, "--enabled/--disabled", help="The global switch."
105
+ ),
106
+ description: str | None = typer.Option(None, "--description"),
107
+ value: str | None = typer.Option(
108
+ None, "--value", help="Default value; an empty string clears it."
109
+ ),
110
+ add_user: str | None = typer.Option(
111
+ None, "--add-user", help="User id (Cognito sub) to put on the allow-list."
112
+ ),
113
+ remove_user: str | None = typer.Option(
114
+ None, "--remove-user", help="User id to take off the allow-list."
115
+ ),
116
+ ) -> None:
117
+ """Change a feature flag."""
118
+ body: dict[str, Any] = {}
119
+ if enabled is not None:
120
+ body["enabled"] = enabled
121
+ if description is not None:
122
+ body["description"] = description.strip()
123
+ if value is not None:
124
+ body["value"] = value.strip()
125
+ if add_user and add_user.strip():
126
+ body["add_user_id"] = add_user.strip()
127
+ if remove_user and remove_user.strip():
128
+ body["remove_user_id"] = remove_user.strip()
129
+ if not body:
130
+ raise typer.BadParameter(
131
+ "pass at least one of --enabled/--disabled, --description, --value, "
132
+ "--add-user, --remove-user"
133
+ )
134
+ flag = _flag_key(key)
135
+ print_json_or_exit(console, lambda: api_client.admin_update_feature_flag(flag, body))
136
+
137
+ @command(
138
+ app,
139
+ "admin",
140
+ "delete",
141
+ "feature-flag",
142
+ api="DELETE /admin/feature-flags/{key}",
143
+ side_effects=["every check of the flag reads false from then on"],
144
+ approval="ask",
145
+ reversible=False,
146
+ idempotent=True,
147
+ help=(
148
+ "DELETE /admin/feature-flags/{key} — remove a flag. Every check of it then reads "
149
+ "false, so delete it only once the code that reads it is gone."
150
+ ),
151
+ )
152
+ def delete_feature_flag(
153
+ key: str = typer.Argument(..., help="Flag key."),
154
+ ) -> None:
155
+ """Delete a feature flag."""
156
+ flag = _flag_key(key)
157
+ print_json_or_exit(console, lambda: api_client.admin_delete_feature_flag(flag))