makea-cli 0.2.2__py3-none-any.whl → 0.2.4__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.2"
1
+ __version__ = "0.2.4"
makea_cli/api_client.py CHANGED
@@ -12,6 +12,7 @@ import httpx
12
12
  from contextvars import ContextVar
13
13
  from dataclasses import dataclass
14
14
 
15
+ from makea_cli import broker
15
16
  from makea_cli.auth_pkce import ensure_fresh_id_token
16
17
  from makea_cli.config import api_base_url
17
18
  from makea_cli.errors import ApiError, AuthError, UnknownOutcomeError
@@ -53,8 +54,20 @@ def _token() -> str:
53
54
  raise AuthError(str(e)) from e
54
55
 
55
56
 
57
+ def _auth_headers() -> dict[str, str]:
58
+ """Who this request is from.
59
+
60
+ Under a token broker the token is a delegation the backend reads from the
61
+ impersonation header; there is no bearer at all, so a command that did not
62
+ come through the host's gate has nothing to send.
63
+ """
64
+ if broker.broker_path():
65
+ return {"x-impersonation-token": broker.brokered_token() or ""}
66
+ return {"Authorization": f"Bearer {_token()}"}
67
+
68
+
56
69
  def _headers() -> dict[str, str]:
57
- headers = {"Authorization": f"Bearer {_token()}", "Accept": "application/json"}
70
+ headers = {**_auth_headers(), "Accept": "application/json"}
58
71
  if state.idempotency_key:
59
72
  headers["Idempotency-Key"] = state.idempotency_key
60
73
  return headers
@@ -254,7 +267,7 @@ def post_multipart_with_files(
254
267
  def _download(path: str, params: dict[str, Any], *, timeout: float) -> tuple[bytes, str | None]:
255
268
  """GET a file (or follow the redirect to its presigned URL); returns (bytes, filename)."""
256
269
  url = f"{api_base_url()}{path}"
257
- headers = {"Authorization": f"Bearer {_token()}", "Accept": "*/*"}
270
+ headers = {**_auth_headers(), "Accept": "*/*"}
258
271
  try:
259
272
  with httpx.Client(timeout=timeout, follow_redirects=True) as client:
260
273
  r = client.get(url, headers=headers, params=params)
@@ -1185,6 +1198,21 @@ def designer_send_rfq(body: dict[str, Any]) -> Any:
1185
1198
  return post_json("/designer/product/send_rfq", body)
1186
1199
 
1187
1200
 
1201
+ def designer_confirm_tech_spec(product_id: str) -> Any:
1202
+ """POST /designer/products/{id}/technical-specifications/confirm — confirm the tech pack."""
1203
+ return post_json(f"/designer/products/{product_id}/technical-specifications/confirm", {})
1204
+
1205
+
1206
+ def designer_award_quotes(body: dict[str, Any]) -> Any:
1207
+ """POST /designer/quotes/group_pay_prepare — DRAFT sampling orders for chosen quotes."""
1208
+ return post_json("/designer/quotes/group_pay_prepare", body)
1209
+
1210
+
1211
+ def designer_create_checkout(body: dict[str, Any]) -> Any:
1212
+ """POST /designer/payments/basket/create-session — a Stripe checkout for charges."""
1213
+ return post_json("/designer/payments/basket/create-session", body)
1214
+
1215
+
1188
1216
  def designer_list_collections() -> Any:
1189
1217
  """GET /designer/collections — the brand's collections."""
1190
1218
  return get_json("/designer/collections")
makea_cli/broker.py ADDED
@@ -0,0 +1,83 @@
1
+ """Token broker client: how the CLI acts for someone without holding their token.
2
+
3
+ A host that runs the CLI on a user's behalf (the Beeboo agent runner) does not
4
+ put that user's token in the environment, where any process in the sandbox
5
+ could read it. It runs a broker on a unix socket and names it in
6
+ ``MAKEA_TOKEN_BROKER``. Before a command runs, the host's gate has already
7
+ looked at the exact command line and left a one-time ticket for it; the CLI
8
+ sends its own argv and gets back a token that is good for this invocation.
9
+
10
+ A process that did not come through the gate (``python -c``, ``curl``, a CLI
11
+ call wrapped in a script) has no ticket and gets nothing. That is the whole
12
+ point, so there is deliberately no fallback to a cached login in this mode.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import socket
20
+ import sys
21
+
22
+ from makea_cli.errors import AuthError
23
+
24
+ BROKER_ENV = "MAKEA_TOKEN_BROKER"
25
+
26
+ # A broker lives on the same box; anything slower than this is a broker that
27
+ # is not there.
28
+ _TIMEOUT_SECONDS = 10.0
29
+
30
+ # One invocation spends one ticket. Every request the command makes reuses the
31
+ # token the ticket bought.
32
+ _cached: dict[str, str] | None = None
33
+
34
+
35
+ def broker_path() -> str | None:
36
+ path = (os.environ.get(BROKER_ENV) or "").strip()
37
+ return path or None
38
+
39
+
40
+ def invocation_argv() -> list[str]:
41
+ """This process's command line, as the gate saw it (without the program name)."""
42
+ return list(sys.argv[1:])
43
+
44
+
45
+ def exchange(path: str, argv: list[str]) -> dict[str, str]:
46
+ """Trade this command line for ``{"token", "persona"}``, or raise AuthError."""
47
+ request = json.dumps({"argv": argv}).encode("utf-8") + b"\n"
48
+ try:
49
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
50
+ sock.settimeout(_TIMEOUT_SECONDS)
51
+ sock.connect(path)
52
+ sock.sendall(request)
53
+ chunks = []
54
+ while True:
55
+ chunk = sock.recv(65536)
56
+ if not chunk:
57
+ break
58
+ chunks.append(chunk)
59
+ if chunk.endswith(b"\n"):
60
+ break
61
+ except OSError as e:
62
+ raise AuthError(f"token broker unavailable ({e.__class__.__name__})") from e
63
+ try:
64
+ reply = json.loads(b"".join(chunks).decode("utf-8") or "{}")
65
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
66
+ raise AuthError("token broker sent an unreadable reply") from e
67
+ if not reply.get("ok") or not reply.get("token"):
68
+ raise AuthError(
69
+ str(reply.get("error") or "the token broker refused this command"),
70
+ detail={"broker": True},
71
+ )
72
+ return {"token": str(reply["token"]), "persona": str(reply.get("persona") or "")}
73
+
74
+
75
+ def brokered_token() -> str | None:
76
+ """The token for this invocation when a broker is configured, else None."""
77
+ global _cached
78
+ path = broker_path()
79
+ if not path:
80
+ return None
81
+ if _cached is None:
82
+ _cached = exchange(path, invocation_argv())
83
+ return _cached["token"]
@@ -817,3 +817,100 @@ def register(app: typer.Typer) -> None:
817
817
  if makea_sourcing:
818
818
  body["makea_sourcing"] = True
819
819
  print_json_or_exit(console, lambda: api_client.designer_send_rfq(body))
820
+
821
+ # --- the sampling journey: confirm the pack, award a quote, pay -------------
822
+
823
+ @command(
824
+ app,
825
+ "designer",
826
+ "submit",
827
+ "tech-pack",
828
+ api="POST /designer/products/{product_id}/technical-specifications/confirm",
829
+ side_effects=[
830
+ "locks the tech pack as the version suppliers quote against",
831
+ "opens the product for sourcing",
832
+ ],
833
+ approval="ask",
834
+ help=(
835
+ "POST /designer/products/{id}/technical-specifications/confirm — confirm the "
836
+ "tech pack, the step before anyone is asked to quote."
837
+ ),
838
+ )
839
+ def submit_tech_pack(
840
+ product_id: str = typer.Argument(..., help="Product id."),
841
+ ) -> None:
842
+ """Confirm a product's tech pack."""
843
+ print_json_or_exit(console, lambda: api_client.designer_confirm_tech_spec(product_id))
844
+
845
+ @command(
846
+ app,
847
+ "designer",
848
+ "create",
849
+ "quote-award",
850
+ api="POST /designer/quotes/group_pay_prepare",
851
+ side_effects=[
852
+ "creates (or reuses) a DRAFT sampling order on each chosen quote",
853
+ "nothing is charged until that order is paid",
854
+ ],
855
+ approval="ask",
856
+ help=(
857
+ "POST /designer/quotes/group_pay_prepare — choose the supplier quote to sample "
858
+ "with. --quote is the supplier link id from `designer list supplier-link`; "
859
+ "repeat --option for the quote's optional extras. Prints the sampling_order_ids "
860
+ "to pay with `designer create checkout --key sampling:<id>`."
861
+ ),
862
+ )
863
+ def create_quote_award(
864
+ product_id: str = typer.Argument(..., help="Product id."),
865
+ quote: str = typer.Option(..., "--quote", help="Supplier link id of the chosen quote."),
866
+ option: list[str] = typer.Option(
867
+ [], "--option", help="Optional extra on the quote to include (repeat)."
868
+ ),
869
+ no_base_sampling: bool = typer.Option(
870
+ False, "--no-base-sampling", help="Leave the quote's base sample out."
871
+ ),
872
+ notes: str | None = typer.Option(None, "--notes", help="Note for the supplier."),
873
+ ) -> None:
874
+ """Award a quote: the sampling order that pays for it."""
875
+ selection: dict[str, Any] = {"product_id": product_id, "link_id": quote}
876
+ if option:
877
+ selection["selected_option_ids"] = list(option)
878
+ if no_base_sampling:
879
+ selection["include_base_sampling"] = False
880
+ if notes:
881
+ selection["notes"] = notes
882
+ print_json_or_exit(
883
+ console, lambda: api_client.designer_award_quotes({"selections": [selection]})
884
+ )
885
+
886
+ @command(
887
+ app,
888
+ "designer",
889
+ "create",
890
+ "checkout",
891
+ api="POST /designer/payments/basket/create-session",
892
+ side_effects=[
893
+ "records the brand's acceptance of the sampling/production terms",
894
+ "creates a Stripe checkout session; nothing is charged until it is paid there",
895
+ ],
896
+ approval="ask",
897
+ help=(
898
+ "POST /designer/payments/basket/create-session — a Stripe checkout link for one "
899
+ "or more charges. --key repeats: sampling:<sampling_order_id>, "
900
+ "production:<production_order_id>:deposit|final, shipping:<id>, misc:<id> "
901
+ "(the `key` of each payable in `designer list payment`). Prints checkout_url."
902
+ ),
903
+ )
904
+ def create_checkout(
905
+ key: list[str] = typer.Option(..., "--key", help="Charge key to pay (repeat)."),
906
+ accept_terms: bool = typer.Option(
907
+ False,
908
+ "--accept-terms",
909
+ help="The brand accepts the sampling/production terms for these charges.",
910
+ ),
911
+ ) -> None:
912
+ """Create a Stripe checkout link for charges."""
913
+ body: dict[str, Any] = {"keys": list(key)}
914
+ if accept_terms:
915
+ body["terms_accepted"] = True
916
+ print_json_or_exit(console, lambda: api_client.designer_create_checkout(body))
@@ -688,7 +688,8 @@ def register(app: typer.Typer) -> None:
688
688
  Read one quote request in full.
689
689
  """
690
690
  print_json_or_exit(
691
- console, lambda: api_client.get_quote_request(quote_request_id=quote_request_id)
691
+ console,
692
+ lambda: api_client.get_quote_request(quote_request_id=quote_request_id),
692
693
  )
693
694
 
694
695
  @command(
@@ -988,9 +989,18 @@ def register(app: typer.Typer) -> None:
988
989
  help=(
989
990
  "ip_schedule: who owns each asset the job creates (optional, JSON object). "
990
991
  "Omit when the factory did not state it — makea must not invent an answer "
991
- "on its behalf. If supplied it has to be COMPLETE (every one of the 12 "
992
- "assets answered, plus production location and subcontractors on a "
993
- "PRODUCTION link) or the API returns 400 listing what is missing."
992
+ "on its behalf. If supplied it has to be COMPLETE or the API returns 400 "
993
+ "listing what is missing. Schema v2: "
994
+ '{"schema_version":"2","background_ip_used":false,"assets":['
995
+ '{"asset_type":"production_files","applicable":true,'
996
+ '"transfer":"transfers_to_client","delivery":"on_full_payment"},'
997
+ '{"asset_type":"physical_tooling","applicable":false}]} '
998
+ "-- both asset types answered (applicable false, the default, is a valid "
999
+ "answer; production_files includes the tech pack and BOM), "
1000
+ "background_ip_used true needs background_ip_declaration, and a "
1001
+ "PRODUCTION link also needs production_location and subcontractors_declared. "
1002
+ "To hide it from the customer afterwards, update the quote with "
1003
+ "--ip-schedule-hidden."
994
1004
  ),
995
1005
  ),
996
1006
  resample_request_id: str | None = typer.Option(
@@ -1250,6 +1260,14 @@ def register(app: typer.Typer) -> None:
1250
1260
  "the factory could not have submitted itself."
1251
1261
  ),
1252
1262
  ),
1263
+ ip_schedule_hidden: bool | None = typer.Option(
1264
+ None,
1265
+ "--ip-schedule-hidden/--ip-schedule-visible",
1266
+ help=(
1267
+ "ip_schedule_hidden_from_customer: hide this quote's IP Schedule from "
1268
+ "the customer, or show it again. Visible unless set."
1269
+ ),
1270
+ ),
1253
1271
  available_sizes_json: str | None = typer.Option(
1254
1272
  None,
1255
1273
  "--available-sizes-json",
@@ -1350,6 +1368,8 @@ def register(app: typer.Typer) -> None:
1350
1368
  )
1351
1369
  if ip_schedule_json is not None:
1352
1370
  override["ip_schedule"] = _parse_json_option(ip_schedule_json, "--ip-schedule-json")
1371
+ if ip_schedule_hidden is not None:
1372
+ override["ip_schedule_hidden_from_customer"] = ip_schedule_hidden
1353
1373
  if refundable_terms_json is not None:
1354
1374
  override["refundable_terms"] = _parse_json_option(
1355
1375
  refundable_terms_json, "--refundable-terms-json"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: makea-cli
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -144,9 +144,11 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
144
144
 
145
145
  | 命令 | 读/写 | 后端 | 说明 | 旧名 |
146
146
  |---|---|---|---|---|
147
+ | `makea-cli designer create checkout` | write(ask) | `POST /designer/payments/basket/create-session` | a Stripe checkout link for one or more charges. --key repeats: sampling:<sampling_order_id>, production:<production_o… | — |
147
148
  | `makea-cli designer create collection` | write(auto) | `POST /designer/collections/create` | create or update a designer collection. | `designer-create-collection` |
148
149
  | `makea-cli designer create note` | write(ask) | `POST /designer/products/{product_id}/notes` | post a message into a product's conversation. | — |
149
150
  | `makea-cli designer create product` | write(ask) | `POST /designer/product/create_or_update_product` | payload must match manufacture/specifications (all frontend fields; categories/subcategories + collection_id in --hel… | `designer-upsert-product` |
151
+ | `makea-cli designer create quote-award` | write(ask) | `POST /designer/quotes/group_pay_prepare` | choose the supplier quote to sample with. --quote is the supplier link id from `designer list supplier-link`; repeat … | — |
150
152
  | `makea-cli designer create rfq` | write(ask) | `POST /designer/product/send_rfq` | ask the chosen suppliers to quote on a product. --supplier repeats; --makea-sourcing also asks Makea to source. | — |
151
153
  | `makea-cli designer create sampling-order` | write(ask) | `POST /designer/sampling_orders` | order samples. --file is the full order payload (product_order_id, items, fees …); run --dry-run first and read the b… | — |
152
154
  | `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
@@ -173,6 +175,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
173
175
  | `makea-cli designer set product-collection` | write(auto) | `POST /designer/products/{id}/link-to-collection` | assign product to a collection. | `designer-link-product-to-collection` |
174
176
  | `makea-cli designer submit production-order` | write(ask) | `POST /designer/production_orders/{production_order_id}/submit` | submit a 大货 order. This is the commitment, so --dry-run and confirm with the user first. | — |
175
177
  | `makea-cli designer submit sampling-order` | write(ask) | `POST /designer/sampling_orders/{sampling_order_id}/confirm` | confirm a sample order so the supplier starts. Run --dry-run first and read it back to the user. | — |
178
+ | `makea-cli designer submit tech-pack` | write(ask) | `POST /designer/products/{product_id}/technical-specifications/confirm` | confirm the tech pack, the step before anyone is asked to quote. | — |
176
179
  | `makea-cli designer update action` | write(auto) | `POST /designer/actions/{action_id}/{acknowledge\|snooze\|dismiss}` | respond to one of the pending actions from `designer list action`. An action is closed by the fact it asks for, not b… | — |
177
180
  | `makea-cli designer update note` | write(ask) | `POST /designer/products/{product_id}/notes/{note_id}/{accept\|reject}` | decide on a task the assistant proposed on a product. | — |
178
181
  | `makea-cli designer update product` | write(auto) | `POST /designer/product/update_basic_info` | patch fields on a product. --cover-image / --back-image take a document id and are what the assistant's set_product_i… | — |
@@ -1,7 +1,8 @@
1
- makea_cli/__init__.py,sha256=m6kyaNpwBcP1XYcqrelX2oS3PJuOnElOcRdBa9pEb8c,22
1
+ makea_cli/__init__.py,sha256=SBl2EPFW-ltPvQ7vbVWItyAsz3aKYIpjO7vcfr84GkU,22
2
2
  makea_cli/__main__.py,sha256=AkQjxWfKE3w9iD1d6I-2Zqvv2t9cct0oYyFF0UiA-Z0,144
3
- makea_cli/api_client.py,sha256=cvs7ntOqVb8mDGiCOmyHwPo2EvwDTYqlchm5pXbG15w,48435
3
+ makea_cli/api_client.py,sha256=rm_0M09oNte-4U_E81k2L9JeHd0NfampHOoH0Ub5-A4,49562
4
4
  makea_cli/auth_pkce.py,sha256=iXSyJ7DM3qVuj-3FwPiqM25CxXz9ebFs9FNthZdw3ws,7315
5
+ makea_cli/broker.py,sha256=5O650nAdRuF-u9E1MuVM2jVv796iBdxJGIaJasxHij0,3019
5
6
  makea_cli/config.py,sha256=lVPsp-44V9pPyUNbcG-uQNV8srCrBVMZnb0HF38Paks,2906
6
7
  makea_cli/errors.py,sha256=_2qKjvkU8YWeBcUd04Yn62lKkZoqQY52kH8klNMyxhM,2780
7
8
  makea_cli/identity.py,sha256=pfYT-asaY1ZTzoAzfqI-v4Eyz1RtxqR2YgvxRGx_lxg,2758
@@ -16,7 +17,7 @@ makea_cli/commands/analytics/cmd.py,sha256=_I5fBlomTalAxRpB-jculS01AbjEC5cVG3lAN
16
17
  makea_cli/commands/auth/__init__.py,sha256=NXoKGfkY2X_hb8GnAyzhoI_qDXfpwKZPqt4HnOrXW3Y,73
17
18
  makea_cli/commands/auth/cmd.py,sha256=G9ZeiaJwzUUTptIyynr0V-82wI7eV0qVX0pD6rdPSXg,1321
18
19
  makea_cli/commands/brand/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- makea_cli/commands/brand/cmd.py,sha256=9Gx2agSJP6LyDZYOGyYr3mAfqPFJRiuno41DmucKZqI,31040
20
+ makea_cli/commands/brand/cmd.py,sha256=WLX3zixuUbQCT6gMT2hMg5qlRgyNJDWYDi15dTsQ99g,35050
20
21
  makea_cli/commands/designer/__init__.py,sha256=Tc3Ju4-BKX2euBljhlrEd-Ksirk6JSPNixq4x0IbQYI,77
21
22
  makea_cli/commands/designer/cmd.py,sha256=4oOJ5tWYojUs8FBj3QRmgpwbicpsU1XqswoNlJpLLS8,23615
22
23
  makea_cli/commands/directory/__init__.py,sha256=rQHBc0JOFXynlzrBm_YfqoRuNsS-JOl-OOvPjgDbnIA,78
@@ -39,8 +40,8 @@ makea_cli/commands/production/__init__.py,sha256=Whn5ziwVMt4jzcokiFWLhWIpRLYC-ld
39
40
  makea_cli/commands/production/cmd.py,sha256=s6fVAGzOvokF6Va5WKGEA2vw2x2hDQ3PrsF01EyZA-Q,38283
40
41
  makea_cli/commands/production/pricing_spec.py,sha256=xGbj7-ps3PcKuU6Oc59A70b62FjF6Awhr5IY3ymZveU,27174
41
42
  makea_cli/commands/supplier/__init__.py,sha256=qfQDeQdQ-B7I_uwFVfcp1vXFUTL4fAcNLu3ipsZ7dfs,77
42
- makea_cli/commands/supplier/cmd.py,sha256=CrG1m0U5-dvdJi4vk9-JRSYY_moPfV_Ud6jRTf-G5Z8,56947
43
- makea_cli-0.2.2.dist-info/METADATA,sha256=jW5EW6HvLmvM2Zrr1Afg6kJTZgXq-0P9Wz0qPKcJGyQ,29877
44
- makea_cli-0.2.2.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
45
- makea_cli-0.2.2.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
46
- makea_cli-0.2.2.dist-info/RECORD,,
43
+ makea_cli/commands/supplier/cmd.py,sha256=crRCy9WfhkryHMIL3iufoiT90_SG3-mKChAtdhd06uc,58076
44
+ makea_cli-0.2.4.dist-info/METADATA,sha256=YNBpGwq6K0GXZF9xNELy1AOgvJykyKD2w7VcuE1ryHs,30554
45
+ makea_cli-0.2.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
46
+ makea_cli-0.2.4.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
47
+ makea_cli-0.2.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.3
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any