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 +1 -1
- makea_cli/api_client.py +132 -0
- makea_cli/commands/__init__.py +2 -0
- makea_cli/commands/brand/cmd.py +84 -0
- makea_cli/commands/directory/cmd.py +14 -0
- makea_cli/commands/flags/__init__.py +0 -0
- makea_cli/commands/flags/cmd.py +157 -0
- makea_cli/commands/orders/cmd.py +252 -0
- makea_cli/commands/product/cmd.py +246 -0
- makea_cli/commands/supplier/cmd.py +150 -0
- makea_cli/companion.py +278 -0
- makea_cli/main.py +64 -3
- makea_cli/output.py +9 -1
- {makea_cli-0.2.24.dist-info → makea_cli-0.2.26.dist-info}/METADATA +23 -1
- {makea_cli-0.2.24.dist-info → makea_cli-0.2.26.dist-info}/RECORD +17 -14
- {makea_cli-0.2.24.dist-info → makea_cli-0.2.26.dist-info}/WHEEL +0 -0
- {makea_cli-0.2.24.dist-info → makea_cli-0.2.26.dist-info}/entry_points.txt +0 -0
makea_cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
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(
|
makea_cli/commands/__init__.py
CHANGED
|
@@ -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)
|
makea_cli/commands/brand/cmd.py
CHANGED
|
@@ -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))
|