codespar 0.10.0__tar.gz → 0.10.2__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.
- {codespar-0.10.0 → codespar-0.10.2}/PKG-INFO +1 -1
- {codespar-0.10.0 → codespar-0.10.2}/pyproject.toml +1 -1
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/__init__.py +59 -1
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/_async_client.py +10 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/_async_session.py +268 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/_sync_client.py +18 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/types.py +323 -0
- codespar-0.10.2/tests/_fixtures/shop_canonical.json +52 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_config.py +10 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_session.py +3 -3
- codespar-0.10.2/tests/test_shop.py +212 -0
- {codespar-0.10.0 → codespar-0.10.2}/.gitignore +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/CHANGELOG.md +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/README.md +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/README.md +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/async_basic.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/connect_link.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/ecommerce_checkout.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/mocks_round_trip.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/pix_payment.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/examples/proxy_execute.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/_http.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/_presets.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/errors.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/py.typed +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/src/codespar/tool_result_codes.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/__init__.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/_fixtures/mocks_canonical.json +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_base_url_resolution.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_error_code_precedence.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_forward_mocks.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_mocks_types.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_streaming.py +0 -0
- {codespar-0.10.0 → codespar-0.10.2}/tests/test_tool_result_codes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codespar
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
4
4
|
Summary: Python SDK for CodeSpar — commerce infrastructure for AI agents in Latin America.
|
|
5
5
|
Project-URL: Homepage, https://codespar.dev
|
|
6
6
|
Project-URL: Documentation, https://docs.codespar.dev
|
|
@@ -58,6 +58,7 @@ from .tool_result_codes import (
|
|
|
58
58
|
is_tool_not_mocked,
|
|
59
59
|
)
|
|
60
60
|
from .types import (
|
|
61
|
+
AgenticReceipt,
|
|
61
62
|
AssistantTextEvent,
|
|
62
63
|
AuthConfig,
|
|
63
64
|
AuthResult,
|
|
@@ -78,6 +79,15 @@ from .types import (
|
|
|
78
79
|
DoneEvent,
|
|
79
80
|
ErrorEvent,
|
|
80
81
|
HttpMethod,
|
|
82
|
+
IssueAction,
|
|
83
|
+
IssueArgs,
|
|
84
|
+
IssueResult,
|
|
85
|
+
LedgerAction,
|
|
86
|
+
LedgerArgs,
|
|
87
|
+
LedgerLeg,
|
|
88
|
+
LedgerReceiptResult,
|
|
89
|
+
LedgerReceiptsResult,
|
|
90
|
+
LedgerResult,
|
|
81
91
|
ManageConnections,
|
|
82
92
|
MockObject,
|
|
83
93
|
MockValue,
|
|
@@ -87,6 +97,11 @@ from .types import (
|
|
|
87
97
|
Preset,
|
|
88
98
|
ProxyRequest,
|
|
89
99
|
ProxyResult,
|
|
100
|
+
ReceiptDelivery,
|
|
101
|
+
ReceiptException,
|
|
102
|
+
ReceiptMandate,
|
|
103
|
+
ReceiptPayment,
|
|
104
|
+
ReceiptQuote,
|
|
90
105
|
RequiredSecret,
|
|
91
106
|
SearchStrategy,
|
|
92
107
|
SendResult,
|
|
@@ -100,6 +115,18 @@ from .types import (
|
|
|
100
115
|
ShipItem,
|
|
101
116
|
ShipResult,
|
|
102
117
|
ShipServiceLevel,
|
|
118
|
+
ShopAction,
|
|
119
|
+
ShopAddress,
|
|
120
|
+
ShopArgs,
|
|
121
|
+
ShopBuyer,
|
|
122
|
+
ShopCheckoutItem,
|
|
123
|
+
ShopCheckoutResult,
|
|
124
|
+
ShopCheckoutStatus,
|
|
125
|
+
ShopOffer,
|
|
126
|
+
ShopResult,
|
|
127
|
+
ShopSearchResult,
|
|
128
|
+
ShopStatusResult,
|
|
129
|
+
ShopVariant,
|
|
103
130
|
StreamEvent,
|
|
104
131
|
Tool,
|
|
105
132
|
ToolCallRecord,
|
|
@@ -113,7 +140,7 @@ from .types import (
|
|
|
113
140
|
WizardAction,
|
|
114
141
|
)
|
|
115
142
|
|
|
116
|
-
__version__ = "0.10.
|
|
143
|
+
__version__ = "0.10.2"
|
|
117
144
|
|
|
118
145
|
__all__ = [
|
|
119
146
|
"APPROVAL_REQUIRED",
|
|
@@ -122,6 +149,8 @@ __all__ = [
|
|
|
122
149
|
"POLICY_DENIED",
|
|
123
150
|
"TOOL_NOT_MOCKED",
|
|
124
151
|
"TOOL_RESULT_CODES",
|
|
152
|
+
# Agentic receipt (the Control Record)
|
|
153
|
+
"AgenticReceipt",
|
|
125
154
|
"ApiError",
|
|
126
155
|
"ApprovalRequiredOutput",
|
|
127
156
|
"AssistantTextEvent",
|
|
@@ -155,6 +184,17 @@ __all__ = [
|
|
|
155
184
|
"DoneEvent",
|
|
156
185
|
"ErrorEvent",
|
|
157
186
|
"HttpMethod",
|
|
187
|
+
# Issue (codespar_issue)
|
|
188
|
+
"IssueAction",
|
|
189
|
+
"IssueArgs",
|
|
190
|
+
"IssueResult",
|
|
191
|
+
# Ledger (codespar_ledger)
|
|
192
|
+
"LedgerAction",
|
|
193
|
+
"LedgerArgs",
|
|
194
|
+
"LedgerLeg",
|
|
195
|
+
"LedgerReceiptResult",
|
|
196
|
+
"LedgerReceiptsResult",
|
|
197
|
+
"LedgerResult",
|
|
158
198
|
"ManageConnections",
|
|
159
199
|
# Test-mode mocks
|
|
160
200
|
"MockObject",
|
|
@@ -171,6 +211,11 @@ __all__ = [
|
|
|
171
211
|
# Proxy
|
|
172
212
|
"ProxyRequest",
|
|
173
213
|
"ProxyResult",
|
|
214
|
+
"ReceiptDelivery",
|
|
215
|
+
"ReceiptException",
|
|
216
|
+
"ReceiptMandate",
|
|
217
|
+
"ReceiptPayment",
|
|
218
|
+
"ReceiptQuote",
|
|
174
219
|
"RequiredSecret",
|
|
175
220
|
"SearchStrategy",
|
|
176
221
|
"SendResult",
|
|
@@ -188,6 +233,19 @@ __all__ = [
|
|
|
188
233
|
"ShipItem",
|
|
189
234
|
"ShipResult",
|
|
190
235
|
"ShipServiceLevel",
|
|
236
|
+
# Shopping (codespar_shop)
|
|
237
|
+
"ShopAction",
|
|
238
|
+
"ShopAddress",
|
|
239
|
+
"ShopArgs",
|
|
240
|
+
"ShopBuyer",
|
|
241
|
+
"ShopCheckoutItem",
|
|
242
|
+
"ShopCheckoutResult",
|
|
243
|
+
"ShopCheckoutStatus",
|
|
244
|
+
"ShopOffer",
|
|
245
|
+
"ShopResult",
|
|
246
|
+
"ShopSearchResult",
|
|
247
|
+
"ShopStatusResult",
|
|
248
|
+
"ShopVariant",
|
|
191
249
|
"StreamError",
|
|
192
250
|
# Streaming events
|
|
193
251
|
"StreamEvent",
|
|
@@ -11,6 +11,7 @@ having to write ``async def``.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
|
+
import re
|
|
14
15
|
from types import TracebackType
|
|
15
16
|
|
|
16
17
|
import httpx
|
|
@@ -25,6 +26,11 @@ from ._presets import preset_to_servers
|
|
|
25
26
|
from .errors import ApiError, ConfigError
|
|
26
27
|
from .types import SessionConfig
|
|
27
28
|
|
|
29
|
+
# Wire format for a project scope. Kept in sync with the TS SDK
|
|
30
|
+
# (`PROJECT_ID_REGEX` in packages/core/src/types.ts) so both clients
|
|
31
|
+
# reject the same project_id inputs.
|
|
32
|
+
_PROJECT_ID_RE = re.compile(r"^prj_[A-Za-z0-9]{16}$")
|
|
33
|
+
|
|
28
34
|
|
|
29
35
|
def _resolve_base_url(explicit: str | None) -> str:
|
|
30
36
|
"""Resolve the effective base URL.
|
|
@@ -67,6 +73,10 @@ class AsyncCodeSpar:
|
|
|
67
73
|
"api_key is required and must start with 'csk_'. "
|
|
68
74
|
"Get one from https://dashboard.codespar.dev."
|
|
69
75
|
)
|
|
76
|
+
if project_id is not None and not _PROJECT_ID_RE.match(project_id):
|
|
77
|
+
raise ConfigError(
|
|
78
|
+
"project_id must match ^prj_[A-Za-z0-9]{16}$ (e.g. 'prj_...')."
|
|
79
|
+
)
|
|
70
80
|
self._api_key = api_key
|
|
71
81
|
self._base_url = _resolve_base_url(base_url).rstrip("/")
|
|
72
82
|
self._project_id = project_id
|
|
@@ -35,6 +35,10 @@ from .types import (
|
|
|
35
35
|
DiscoverToolMatch,
|
|
36
36
|
DoneEvent,
|
|
37
37
|
ErrorEvent,
|
|
38
|
+
IssueArgs,
|
|
39
|
+
IssueResult,
|
|
40
|
+
LedgerArgs,
|
|
41
|
+
LedgerResult,
|
|
38
42
|
PaymentStatusEvent,
|
|
39
43
|
PaymentStatusResult,
|
|
40
44
|
ProxyRequest,
|
|
@@ -45,6 +49,13 @@ from .types import (
|
|
|
45
49
|
SessionInfo,
|
|
46
50
|
ShipArgs,
|
|
47
51
|
ShipResult,
|
|
52
|
+
ShopArgs,
|
|
53
|
+
ShopCheckoutResult,
|
|
54
|
+
ShopOffer,
|
|
55
|
+
ShopResult,
|
|
56
|
+
ShopSearchResult,
|
|
57
|
+
ShopStatusResult,
|
|
58
|
+
ShopVariant,
|
|
48
59
|
StreamEvent,
|
|
49
60
|
Tool,
|
|
50
61
|
ToolCallRecord,
|
|
@@ -196,6 +207,91 @@ def _parse_ship_result(raw: dict[str, Any]) -> ShipResult:
|
|
|
196
207
|
)
|
|
197
208
|
|
|
198
209
|
|
|
210
|
+
def _parse_ledger_result(raw: dict[str, Any]) -> LedgerResult:
|
|
211
|
+
return LedgerResult(
|
|
212
|
+
id=raw.get("id"),
|
|
213
|
+
status=raw.get("status"),
|
|
214
|
+
account_id=raw.get("account_id"),
|
|
215
|
+
alias=raw.get("alias"),
|
|
216
|
+
balances=raw.get("balances"),
|
|
217
|
+
raw=raw.get("raw"),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _parse_issue_result(raw: dict[str, Any]) -> IssueResult:
|
|
222
|
+
return IssueResult(
|
|
223
|
+
id=raw.get("id"),
|
|
224
|
+
status=raw.get("status"),
|
|
225
|
+
card_type=raw.get("card_type"),
|
|
226
|
+
last_four=raw.get("last_four"),
|
|
227
|
+
cardholder_id=raw.get("cardholder_id"),
|
|
228
|
+
program_id=raw.get("program_id"),
|
|
229
|
+
raw=raw.get("raw"),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse_shop_variant(raw: dict[str, Any]) -> ShopVariant:
|
|
234
|
+
return ShopVariant(
|
|
235
|
+
sku_id=str(raw.get("sku_id", "")),
|
|
236
|
+
available=bool(raw.get("available", False)),
|
|
237
|
+
title=raw.get("title"),
|
|
238
|
+
price_minor=raw.get("price_minor"),
|
|
239
|
+
currency=raw.get("currency"),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_shop_offer(raw: dict[str, Any]) -> ShopOffer:
|
|
244
|
+
variants_raw = raw.get("variants") or []
|
|
245
|
+
return ShopOffer(
|
|
246
|
+
product_id=str(raw.get("product_id", "")),
|
|
247
|
+
available=bool(raw.get("available", False)),
|
|
248
|
+
variants=[
|
|
249
|
+
_parse_shop_variant(v) for v in variants_raw if isinstance(v, dict)
|
|
250
|
+
],
|
|
251
|
+
sku_id=raw.get("sku_id"),
|
|
252
|
+
title=raw.get("title"),
|
|
253
|
+
price_minor=raw.get("price_minor"),
|
|
254
|
+
currency=raw.get("currency"),
|
|
255
|
+
image=raw.get("image"),
|
|
256
|
+
url=raw.get("url"),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_shop_result(raw: dict[str, Any], action: str) -> ShopResult:
|
|
261
|
+
"""Map the wire payload to the action-correct dataclass.
|
|
262
|
+
|
|
263
|
+
The wire shape is discriminated by the call's ``action`` (search →
|
|
264
|
+
products, checkout → in_progress session, checkout_status → terminal
|
|
265
|
+
poll), mirroring the TS discriminated ``ShopResult`` union.
|
|
266
|
+
"""
|
|
267
|
+
if action == "search":
|
|
268
|
+
products_raw = raw.get("products") or []
|
|
269
|
+
return ShopSearchResult(
|
|
270
|
+
rail=str(raw.get("rail", "")),
|
|
271
|
+
products=[
|
|
272
|
+
_parse_shop_offer(p)
|
|
273
|
+
for p in products_raw
|
|
274
|
+
if isinstance(p, dict)
|
|
275
|
+
],
|
|
276
|
+
)
|
|
277
|
+
if action == "checkout":
|
|
278
|
+
return ShopCheckoutResult(
|
|
279
|
+
checkout_session_id=str(raw.get("checkout_session_id", "")),
|
|
280
|
+
status="in_progress",
|
|
281
|
+
message=raw.get("message"),
|
|
282
|
+
)
|
|
283
|
+
# checkout_status
|
|
284
|
+
return ShopStatusResult(
|
|
285
|
+
checkout_session_id=str(raw.get("checkout_session_id", "")),
|
|
286
|
+
status=raw.get("status", "in_progress"),
|
|
287
|
+
rail=raw.get("rail"),
|
|
288
|
+
total_minor=raw.get("total_minor"),
|
|
289
|
+
pix_copia_e_cola=raw.get("pix_copia_e_cola"),
|
|
290
|
+
order_status=raw.get("order_status"),
|
|
291
|
+
error=raw.get("error"),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
199
295
|
def _parse_stream_event(raw: dict[str, Any]) -> StreamEvent | None:
|
|
200
296
|
"""
|
|
201
297
|
Normalize a raw SSE payload into a typed ``StreamEvent``.
|
|
@@ -521,6 +617,178 @@ class AsyncSession:
|
|
|
521
617
|
raise ApiError("ship: malformed response", status=0, body=result.data)
|
|
522
618
|
return _parse_ship_result(result.data)
|
|
523
619
|
|
|
620
|
+
async def ledger(self, args: LedgerArgs) -> LedgerResult:
|
|
621
|
+
"""
|
|
622
|
+
Post a double-entry journal entry, read an account's balances,
|
|
623
|
+
or create an account on the tenant's self-hosted Lerian Midaz
|
|
624
|
+
ledger. Typed wrapper around ``execute("codespar_ledger", {...})``.
|
|
625
|
+
|
|
626
|
+
``args.action`` is one of ``entry`` | ``balance`` | ``account``.
|
|
627
|
+
For ``entry``: asset + source + destination are required (source
|
|
628
|
+
debits must equal destination credits, same asset). For
|
|
629
|
+
``balance``: ``account`` (id) is required. For ``account``:
|
|
630
|
+
``asset`` is required. Amounts are in MINOR units (cents). The
|
|
631
|
+
connection (base_url + org_id + ledger_id) is operator-seeded.
|
|
632
|
+
"""
|
|
633
|
+
params: dict[str, Any] = {"action": args.action}
|
|
634
|
+
if args.asset is not None:
|
|
635
|
+
params["asset"] = args.asset
|
|
636
|
+
if args.scale is not None:
|
|
637
|
+
params["scale"] = args.scale
|
|
638
|
+
if args.source is not None:
|
|
639
|
+
params["source"] = [
|
|
640
|
+
{"account": leg.account, "amount": leg.amount}
|
|
641
|
+
for leg in args.source
|
|
642
|
+
]
|
|
643
|
+
if args.destination is not None:
|
|
644
|
+
params["destination"] = [
|
|
645
|
+
{"account": leg.account, "amount": leg.amount}
|
|
646
|
+
for leg in args.destination
|
|
647
|
+
]
|
|
648
|
+
if args.description is not None:
|
|
649
|
+
params["description"] = args.description
|
|
650
|
+
if args.account is not None:
|
|
651
|
+
params["account"] = args.account
|
|
652
|
+
if args.alias is not None:
|
|
653
|
+
params["alias"] = args.alias
|
|
654
|
+
if args.name is not None:
|
|
655
|
+
params["name"] = args.name
|
|
656
|
+
if args.type is not None:
|
|
657
|
+
params["type"] = args.type
|
|
658
|
+
if args.metadata is not None:
|
|
659
|
+
params["metadata"] = args.metadata
|
|
660
|
+
result = await self.execute("codespar_ledger", params)
|
|
661
|
+
if not result.success:
|
|
662
|
+
raise ApiError(
|
|
663
|
+
f"ledger failed: {result.error or 'unknown'}",
|
|
664
|
+
status=0,
|
|
665
|
+
body=result.error,
|
|
666
|
+
)
|
|
667
|
+
if not isinstance(result.data, dict):
|
|
668
|
+
raise ApiError("ledger: malformed response", status=0, body=result.data)
|
|
669
|
+
return _parse_ledger_result(result.data)
|
|
670
|
+
|
|
671
|
+
async def issue(self, args: IssueArgs) -> IssueResult:
|
|
672
|
+
"""
|
|
673
|
+
Issue a virtual/physical card, control (freeze/unfreeze/cancel)
|
|
674
|
+
one, or read its status on the tenant's Pomelo card-issuing
|
|
675
|
+
program. Typed wrapper around ``execute("codespar_issue", {...})``.
|
|
676
|
+
|
|
677
|
+
``args.action`` is one of ``card-virtual`` | ``card-physical`` |
|
|
678
|
+
``card-control`` | ``card-get``. cardholder_id + program_id are
|
|
679
|
+
required to issue; card_id is required for control + get;
|
|
680
|
+
card-physical needs shipping_address.
|
|
681
|
+
"""
|
|
682
|
+
params: dict[str, Any] = {"action": args.action}
|
|
683
|
+
if args.cardholder_id is not None:
|
|
684
|
+
params["cardholder_id"] = args.cardholder_id
|
|
685
|
+
if args.program_id is not None:
|
|
686
|
+
params["program_id"] = args.program_id
|
|
687
|
+
if args.card_id is not None:
|
|
688
|
+
params["card_id"] = args.card_id
|
|
689
|
+
if args.control is not None:
|
|
690
|
+
params["control"] = args.control
|
|
691
|
+
if args.reason is not None:
|
|
692
|
+
params["reason"] = args.reason
|
|
693
|
+
if args.shipping_address is not None:
|
|
694
|
+
params["shipping_address"] = args.shipping_address
|
|
695
|
+
if args.metadata is not None:
|
|
696
|
+
params["metadata"] = args.metadata
|
|
697
|
+
result = await self.execute("codespar_issue", params)
|
|
698
|
+
if not result.success:
|
|
699
|
+
raise ApiError(
|
|
700
|
+
f"issue failed: {result.error or 'unknown'}",
|
|
701
|
+
status=0,
|
|
702
|
+
body=result.error,
|
|
703
|
+
)
|
|
704
|
+
if not isinstance(result.data, dict):
|
|
705
|
+
raise ApiError("issue: malformed response", status=0, body=result.data)
|
|
706
|
+
return _parse_issue_result(result.data)
|
|
707
|
+
|
|
708
|
+
async def shop(self, args: ShopArgs) -> ShopResult:
|
|
709
|
+
"""
|
|
710
|
+
Buy-side shopping: catalog search → async checkout → Pix mint.
|
|
711
|
+
Typed wrapper around ``execute("codespar_shop", {...})``. The
|
|
712
|
+
result type is action-correct: ``search`` → ``ShopSearchResult``,
|
|
713
|
+
``checkout`` → ``ShopCheckoutResult``, ``checkout_status`` →
|
|
714
|
+
``ShopStatusResult`` (version-locked to the TS discriminated
|
|
715
|
+
``ShopResult`` union).
|
|
716
|
+
|
|
717
|
+
Checkout is async: ``action="checkout"`` returns
|
|
718
|
+
``{checkout_session_id, status:"in_progress"}``; poll
|
|
719
|
+
``action="checkout_status"`` until ``ready_for_payment`` (carries
|
|
720
|
+
``pix_copia_e_cola``) or ``canceled`` (carries ``error``). Settle
|
|
721
|
+
the returned Pix via a separate payment tool — settlement and
|
|
722
|
+
governance are out of this contract.
|
|
723
|
+
|
|
724
|
+
Requires a runtime that implements the ``codespar_shop``
|
|
725
|
+
meta-tool; a self-hosted OSS runtime with no registered
|
|
726
|
+
implementation returns "Tool not registered".
|
|
727
|
+
"""
|
|
728
|
+
params: dict[str, Any] = {"action": args.action}
|
|
729
|
+
if args.action == "search":
|
|
730
|
+
if args.query is not None:
|
|
731
|
+
params["query"] = args.query
|
|
732
|
+
if args.limit is not None:
|
|
733
|
+
params["limit"] = args.limit
|
|
734
|
+
if args.merchant is not None:
|
|
735
|
+
params["merchant"] = args.merchant
|
|
736
|
+
elif args.action == "checkout":
|
|
737
|
+
if args.merchant is not None:
|
|
738
|
+
params["merchant"] = args.merchant
|
|
739
|
+
if args.items is not None:
|
|
740
|
+
params["items"] = [
|
|
741
|
+
{
|
|
742
|
+
"variant_id": it.variant_id,
|
|
743
|
+
**(
|
|
744
|
+
{"quantity": it.quantity}
|
|
745
|
+
if it.quantity is not None
|
|
746
|
+
else {}
|
|
747
|
+
),
|
|
748
|
+
**({"seller": it.seller} if it.seller else {}),
|
|
749
|
+
}
|
|
750
|
+
for it in args.items
|
|
751
|
+
]
|
|
752
|
+
if args.url is not None:
|
|
753
|
+
params["url"] = args.url
|
|
754
|
+
if args.consumer_id is not None:
|
|
755
|
+
params["consumer_id"] = args.consumer_id
|
|
756
|
+
if args.buyer is not None:
|
|
757
|
+
params["buyer"] = {
|
|
758
|
+
**({"name": args.buyer.name} if args.buyer.name else {}),
|
|
759
|
+
**({"email": args.buyer.email} if args.buyer.email else {}),
|
|
760
|
+
**({"cpf": args.buyer.cpf} if args.buyer.cpf else {}),
|
|
761
|
+
**({"phone": args.buyer.phone} if args.buyer.phone else {}),
|
|
762
|
+
}
|
|
763
|
+
if args.address is not None:
|
|
764
|
+
a = args.address
|
|
765
|
+
params["address"] = {
|
|
766
|
+
"cep": a.cep,
|
|
767
|
+
**({"street": a.street} if a.street else {}),
|
|
768
|
+
**({"number": a.number} if a.number else {}),
|
|
769
|
+
**({"complement": a.complement} if a.complement else {}),
|
|
770
|
+
**(
|
|
771
|
+
{"neighborhood": a.neighborhood}
|
|
772
|
+
if a.neighborhood
|
|
773
|
+
else {}
|
|
774
|
+
),
|
|
775
|
+
**({"city": a.city} if a.city else {}),
|
|
776
|
+
**({"state": a.state} if a.state else {}),
|
|
777
|
+
}
|
|
778
|
+
else: # checkout_status
|
|
779
|
+
if args.checkout_session_id is not None:
|
|
780
|
+
params["checkout_session_id"] = args.checkout_session_id
|
|
781
|
+
result = await self.execute("codespar_shop", params)
|
|
782
|
+
if not result.success:
|
|
783
|
+
raise ApiError(
|
|
784
|
+
f"shop failed: {result.error or 'unknown'}",
|
|
785
|
+
status=0,
|
|
786
|
+
body=result.error,
|
|
787
|
+
)
|
|
788
|
+
if not isinstance(result.data, dict):
|
|
789
|
+
raise ApiError("shop: malformed response", status=0, body=result.data)
|
|
790
|
+
return _parse_shop_result(result.data, args.action)
|
|
791
|
+
|
|
524
792
|
async def verification_status(
|
|
525
793
|
self, tool_call_id: str
|
|
526
794
|
) -> VerificationStatusResult:
|
|
@@ -31,6 +31,10 @@ from .types import (
|
|
|
31
31
|
ConnectionWizardResult,
|
|
32
32
|
DiscoverOptions,
|
|
33
33
|
DiscoverResult,
|
|
34
|
+
IssueArgs,
|
|
35
|
+
IssueResult,
|
|
36
|
+
LedgerArgs,
|
|
37
|
+
LedgerResult,
|
|
34
38
|
PaymentStatusResult,
|
|
35
39
|
ProxyRequest,
|
|
36
40
|
ProxyResult,
|
|
@@ -40,6 +44,8 @@ from .types import (
|
|
|
40
44
|
SessionInfo,
|
|
41
45
|
ShipArgs,
|
|
42
46
|
ShipResult,
|
|
47
|
+
ShopArgs,
|
|
48
|
+
ShopResult,
|
|
43
49
|
StreamEvent,
|
|
44
50
|
Tool,
|
|
45
51
|
ToolResult,
|
|
@@ -173,6 +179,18 @@ class Session:
|
|
|
173
179
|
"""Sync wrapper around ``AsyncSession.ship``. See that for docs."""
|
|
174
180
|
return self._runner.run(self._async.ship(args))
|
|
175
181
|
|
|
182
|
+
def ledger(self, args: LedgerArgs) -> LedgerResult:
|
|
183
|
+
"""Sync wrapper around ``AsyncSession.ledger``. See that for docs."""
|
|
184
|
+
return self._runner.run(self._async.ledger(args))
|
|
185
|
+
|
|
186
|
+
def issue(self, args: IssueArgs) -> IssueResult:
|
|
187
|
+
"""Sync wrapper around ``AsyncSession.issue``. See that for docs."""
|
|
188
|
+
return self._runner.run(self._async.issue(args))
|
|
189
|
+
|
|
190
|
+
def shop(self, args: ShopArgs) -> ShopResult:
|
|
191
|
+
"""Sync wrapper around ``AsyncSession.shop``. See that for docs."""
|
|
192
|
+
return self._runner.run(self._async.shop(args))
|
|
193
|
+
|
|
176
194
|
def proxy_execute(self, request: ProxyRequest) -> ProxyResult:
|
|
177
195
|
return self._runner.run(self._async.proxy_execute(request))
|
|
178
196
|
|
|
@@ -459,6 +459,329 @@ class ShipResult:
|
|
|
459
459
|
raw: Any = None
|
|
460
460
|
|
|
461
461
|
|
|
462
|
+
# ── codespar_ledger wire shape ─────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
LedgerAction = Literal["entry", "balance", "account", "receipt", "receipts"]
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass(slots=True)
|
|
469
|
+
class LedgerLeg:
|
|
470
|
+
"""One side of a journal entry. ``account`` is a Midaz account alias
|
|
471
|
+
(e.g. ``@wallet/user_123``); ``amount`` is in minor units (cents)."""
|
|
472
|
+
|
|
473
|
+
account: str
|
|
474
|
+
amount: int
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@dataclass(slots=True)
|
|
478
|
+
class LedgerArgs:
|
|
479
|
+
"""Input shape for ``Session.ledger``.
|
|
480
|
+
|
|
481
|
+
Three actions over a tenant's self-hosted double-entry ledger
|
|
482
|
+
(Lerian Midaz). Mirrors the backend's MetaLedgerArgs so the wire
|
|
483
|
+
payload matches byte-for-byte. The connection (base_url + org_id +
|
|
484
|
+
ledger_id) is operator-seeded, never passed by the agent. Amounts
|
|
485
|
+
are in MINOR units (cents).
|
|
486
|
+
- ``entry`` Post an n:n journal transaction (source debits must
|
|
487
|
+
equal destination credits, same asset)
|
|
488
|
+
- ``balance`` Read an account's balances
|
|
489
|
+
- ``account`` Create an account for an asset
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
action: LedgerAction
|
|
493
|
+
asset: str | None = None
|
|
494
|
+
scale: int | None = None
|
|
495
|
+
source: list[LedgerLeg] | None = None
|
|
496
|
+
destination: list[LedgerLeg] | None = None
|
|
497
|
+
description: str | None = None
|
|
498
|
+
account: str | None = None
|
|
499
|
+
alias: str | None = None
|
|
500
|
+
name: str | None = None
|
|
501
|
+
type: str | None = None
|
|
502
|
+
receipt_id: str | None = None
|
|
503
|
+
consumer_id: str | None = None
|
|
504
|
+
limit: int | None = None
|
|
505
|
+
metadata: dict[str, Any] | None = None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@dataclass(slots=True)
|
|
509
|
+
class LedgerResult:
|
|
510
|
+
id: str | None = None
|
|
511
|
+
status: str | None = None
|
|
512
|
+
account_id: str | None = None
|
|
513
|
+
alias: str | None = None
|
|
514
|
+
balances: Any = None
|
|
515
|
+
raw: Any = None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ── Agentic receipt (the Control Record) ───────────────────────────
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@dataclass(slots=True)
|
|
522
|
+
class ReceiptException:
|
|
523
|
+
"""A settle-time policy divergence recorded on a receipt (OBSERVE mode) —
|
|
524
|
+
e.g. the settled amount/payee diverging from the bound quote."""
|
|
525
|
+
|
|
526
|
+
code: str
|
|
527
|
+
detail: str
|
|
528
|
+
at: str
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@dataclass(slots=True)
|
|
532
|
+
class ReceiptMandate:
|
|
533
|
+
id: str
|
|
534
|
+
nonce: str
|
|
535
|
+
scope: str
|
|
536
|
+
currency: str
|
|
537
|
+
sig: str
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@dataclass(slots=True)
|
|
541
|
+
class ReceiptQuote:
|
|
542
|
+
seller: str | None = None
|
|
543
|
+
resource: str | None = None
|
|
544
|
+
price_minor: int | None = None
|
|
545
|
+
payee: str | None = None
|
|
546
|
+
session_id: str | None = None
|
|
547
|
+
sig: str | None = None
|
|
548
|
+
at: str | None = None
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@dataclass(slots=True)
|
|
552
|
+
class ReceiptPayment:
|
|
553
|
+
rail: str
|
|
554
|
+
amount_minor: int
|
|
555
|
+
attempt_id: str
|
|
556
|
+
money_moved: bool
|
|
557
|
+
provider: str | None = None
|
|
558
|
+
tx_id: str | None = None
|
|
559
|
+
at: str | None = None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@dataclass(slots=True)
|
|
563
|
+
class ReceiptDelivery:
|
|
564
|
+
result: str | None = None
|
|
565
|
+
proof: str | None = None
|
|
566
|
+
kind: str | None = None
|
|
567
|
+
nfe_chave: str | None = None
|
|
568
|
+
at: str | None = None
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@dataclass(slots=True)
|
|
572
|
+
class AgenticReceipt:
|
|
573
|
+
"""The canonical agentic receipt — the Control Record of one spend.
|
|
574
|
+
|
|
575
|
+
Binds mandate -> quote -> payment -> delivery into a tamper-evident
|
|
576
|
+
chain (SHA-256 over the RFC 8785 canonical JSON of the records
|
|
577
|
+
present); ``receipt_sig`` is the consumer HMAC over ``chain``. The
|
|
578
|
+
structure is rail-agnostic; only the delivery proof artifact differs
|
|
579
|
+
(NF-e + Pix endToEndId for the fiscal path, the x402 resource
|
|
580
|
+
response for crypto). Returned by ``codespar_ledger`` action=receipt.
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
receipt_id: str
|
|
584
|
+
state: Literal["paid", "exception", "delivered", "voided"]
|
|
585
|
+
mandate: ReceiptMandate
|
|
586
|
+
payment: ReceiptPayment
|
|
587
|
+
chain: str
|
|
588
|
+
receipt_sig: str
|
|
589
|
+
quote: ReceiptQuote | None = None
|
|
590
|
+
delivery: ReceiptDelivery | None = None
|
|
591
|
+
exceptions: list[ReceiptException] = field(default_factory=list)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@dataclass(slots=True)
|
|
595
|
+
class LedgerReceiptResult:
|
|
596
|
+
"""Result of ``codespar_ledger`` action=receipt. A missing receipt
|
|
597
|
+
returns ``found=False`` (not an error)."""
|
|
598
|
+
|
|
599
|
+
found: bool
|
|
600
|
+
receipt: AgenticReceipt | None = None
|
|
601
|
+
receipt_id: str | None = None
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@dataclass(slots=True)
|
|
605
|
+
class LedgerReceiptsResult:
|
|
606
|
+
"""Result of ``codespar_ledger`` action=receipts."""
|
|
607
|
+
|
|
608
|
+
receipts: list[AgenticReceipt] = field(default_factory=list)
|
|
609
|
+
count: int = 0
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ── codespar_issue wire shape ──────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
IssueAction = Literal["card-virtual", "card-physical", "card-control", "card-get"]
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@dataclass(slots=True)
|
|
619
|
+
class IssueArgs:
|
|
620
|
+
"""Input shape for ``Session.issue`` (Pomelo card issuing).
|
|
621
|
+
|
|
622
|
+
Mirrors the backend's MetaIssueArgs. Asset-agnostic — the program
|
|
623
|
+
currency is set on the card program, not per call.
|
|
624
|
+
- ``card-virtual`` Issue a virtual card (active immediately)
|
|
625
|
+
- ``card-physical`` Issue a physical card (needs shipping_address)
|
|
626
|
+
- ``card-control`` Freeze / unfreeze / cancel an existing card
|
|
627
|
+
- ``card-get`` Read a card's status
|
|
628
|
+
"""
|
|
629
|
+
|
|
630
|
+
action: IssueAction
|
|
631
|
+
cardholder_id: str | None = None
|
|
632
|
+
program_id: str | None = None
|
|
633
|
+
card_id: str | None = None
|
|
634
|
+
control: Literal["freeze", "unfreeze", "cancel"] | None = None
|
|
635
|
+
reason: str | None = None
|
|
636
|
+
shipping_address: dict[str, Any] | None = None
|
|
637
|
+
metadata: dict[str, Any] | None = None
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@dataclass(slots=True)
|
|
641
|
+
class IssueResult:
|
|
642
|
+
id: str | None = None
|
|
643
|
+
status: str | None = None
|
|
644
|
+
card_type: str | None = None
|
|
645
|
+
last_four: str | None = None
|
|
646
|
+
cardholder_id: str | None = None
|
|
647
|
+
program_id: str | None = None
|
|
648
|
+
raw: Any = None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# ── codespar_shop wire shape ───────────────────────────────────────
|
|
652
|
+
|
|
653
|
+
ShopAction = Literal["search", "checkout", "checkout_status"]
|
|
654
|
+
ShopCheckoutStatus = Literal["in_progress", "ready_for_payment", "canceled"]
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@dataclass(slots=True)
|
|
658
|
+
class ShopVariant:
|
|
659
|
+
"""One buyable SKU under a ``ShopOffer``.
|
|
660
|
+
|
|
661
|
+
Note the field-name asymmetry the contract documents (not a bug):
|
|
662
|
+
pass this ``sku_id`` as the checkout item's ``variant_id``. The
|
|
663
|
+
product id is NOT buyable — only the SKU is.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
sku_id: str
|
|
667
|
+
available: bool
|
|
668
|
+
title: str | None = None
|
|
669
|
+
price_minor: int | None = None # integer minor units (centavos)
|
|
670
|
+
currency: str | None = None # ISO-4217, default "BRL"
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@dataclass(slots=True)
|
|
674
|
+
class ShopOffer:
|
|
675
|
+
"""A flattened catalog offer returned by ``search``."""
|
|
676
|
+
|
|
677
|
+
product_id: str
|
|
678
|
+
available: bool
|
|
679
|
+
variants: list[ShopVariant]
|
|
680
|
+
sku_id: str | None = None
|
|
681
|
+
title: str | None = None
|
|
682
|
+
price_minor: int | None = None
|
|
683
|
+
currency: str | None = None
|
|
684
|
+
image: str | None = None
|
|
685
|
+
url: str | None = None
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
@dataclass(slots=True)
|
|
689
|
+
class ShopCheckoutItem:
|
|
690
|
+
"""A line item for a VTEX-rail ``checkout``."""
|
|
691
|
+
|
|
692
|
+
variant_id: str # pass ShopVariant.sku_id here
|
|
693
|
+
quantity: int | None = None # defaults to 1 when omitted
|
|
694
|
+
seller: str | None = None # VTEX marketplace sub-seller id
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
@dataclass(slots=True)
|
|
698
|
+
class ShopBuyer:
|
|
699
|
+
"""Optional vaulted buyer profile, merged with the saved profile."""
|
|
700
|
+
|
|
701
|
+
name: str | None = None
|
|
702
|
+
email: str | None = None
|
|
703
|
+
cpf: str | None = None
|
|
704
|
+
phone: str | None = None
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@dataclass(slots=True)
|
|
708
|
+
class ShopAddress:
|
|
709
|
+
"""Optional vaulted delivery address; ``cep`` required when present."""
|
|
710
|
+
|
|
711
|
+
cep: str
|
|
712
|
+
street: str | None = None
|
|
713
|
+
number: str | None = None
|
|
714
|
+
complement: str | None = None
|
|
715
|
+
neighborhood: str | None = None
|
|
716
|
+
city: str | None = None
|
|
717
|
+
state: str | None = None
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@dataclass(slots=True)
|
|
721
|
+
class ShopArgs:
|
|
722
|
+
"""Input shape for ``Session.shop``.
|
|
723
|
+
|
|
724
|
+
The closed action set is ``search | checkout | checkout_status``
|
|
725
|
+
(default ``search``). Per-action field requirements:
|
|
726
|
+
- ``search``: ``query`` required; ``limit`` clamped 1..20.
|
|
727
|
+
- ``checkout``: ``items`` XOR ``url``, gated by rail (``items``
|
|
728
|
+
for VTEX, ``url`` for the Mercado Livre PDP path).
|
|
729
|
+
- ``checkout_status``: ``checkout_session_id`` required.
|
|
730
|
+
|
|
731
|
+
Mirrors the TS ``ShopArgs`` discriminated union as a single
|
|
732
|
+
dataclass; the ``shop()`` wrapper serializes only the fields the
|
|
733
|
+
given action uses, matching the TS facade's wire payload.
|
|
734
|
+
"""
|
|
735
|
+
|
|
736
|
+
action: ShopAction
|
|
737
|
+
query: str | None = None
|
|
738
|
+
limit: int | None = None
|
|
739
|
+
merchant: str | None = None
|
|
740
|
+
items: list[ShopCheckoutItem] | None = None
|
|
741
|
+
url: str | None = None
|
|
742
|
+
consumer_id: str | None = None
|
|
743
|
+
buyer: ShopBuyer | None = None
|
|
744
|
+
address: ShopAddress | None = None
|
|
745
|
+
checkout_session_id: str | None = None
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@dataclass(slots=True)
|
|
749
|
+
class ShopSearchResult:
|
|
750
|
+
"""Result of ``search``. Zero results is ``products=[]``, not error."""
|
|
751
|
+
|
|
752
|
+
rail: str
|
|
753
|
+
products: list[ShopOffer]
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@dataclass(slots=True)
|
|
757
|
+
class ShopCheckoutResult:
|
|
758
|
+
"""Result of ``checkout`` — always async, status ``in_progress``."""
|
|
759
|
+
|
|
760
|
+
checkout_session_id: str
|
|
761
|
+
status: Literal["in_progress"]
|
|
762
|
+
message: str | None = None
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
@dataclass(slots=True)
|
|
766
|
+
class ShopStatusResult:
|
|
767
|
+
"""Result of ``checkout_status``.
|
|
768
|
+
|
|
769
|
+
``pix_copia_e_cola`` + ``total_minor`` are present only at
|
|
770
|
+
``ready_for_payment``; ``error`` only at ``canceled``.
|
|
771
|
+
"""
|
|
772
|
+
|
|
773
|
+
checkout_session_id: str
|
|
774
|
+
status: ShopCheckoutStatus
|
|
775
|
+
rail: str | None = None
|
|
776
|
+
total_minor: int | None = None
|
|
777
|
+
pix_copia_e_cola: str | None = None
|
|
778
|
+
order_status: str | None = None
|
|
779
|
+
error: str | None = None
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
ShopResult = ShopSearchResult | ShopCheckoutResult | ShopStatusResult
|
|
783
|
+
|
|
784
|
+
|
|
462
785
|
# ── /v1/tool-calls/:id/payment-status wire shape ───────────────────
|
|
463
786
|
|
|
464
787
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"search": {
|
|
3
|
+
"rail": "vtex",
|
|
4
|
+
"products": [
|
|
5
|
+
{
|
|
6
|
+
"product_id": "prod_1",
|
|
7
|
+
"sku_id": "sku_1",
|
|
8
|
+
"title": "Ração para gato 1kg",
|
|
9
|
+
"price_minor": 4990,
|
|
10
|
+
"currency": "BRL",
|
|
11
|
+
"image": "https://example.test/img/sku_1.jpg",
|
|
12
|
+
"url": "https://cobasi.test/p/sku_1",
|
|
13
|
+
"available": true,
|
|
14
|
+
"variants": [
|
|
15
|
+
{
|
|
16
|
+
"sku_id": "sku_1",
|
|
17
|
+
"title": "1kg",
|
|
18
|
+
"price_minor": 4990,
|
|
19
|
+
"currency": "BRL",
|
|
20
|
+
"available": true
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"search_empty": {
|
|
27
|
+
"rail": "vtex",
|
|
28
|
+
"products": []
|
|
29
|
+
},
|
|
30
|
+
"checkout": {
|
|
31
|
+
"checkout_session_id": "cks_abc123",
|
|
32
|
+
"status": "in_progress",
|
|
33
|
+
"message": "checkout started"
|
|
34
|
+
},
|
|
35
|
+
"status_in_progress": {
|
|
36
|
+
"checkout_session_id": "cks_abc123",
|
|
37
|
+
"status": "in_progress"
|
|
38
|
+
},
|
|
39
|
+
"status_ready_for_payment": {
|
|
40
|
+
"checkout_session_id": "cks_abc123",
|
|
41
|
+
"status": "ready_for_payment",
|
|
42
|
+
"rail": "vtex",
|
|
43
|
+
"total_minor": 4990,
|
|
44
|
+
"pix_copia_e_cola": "00020126580014br.gov.bcb.pix0136cks-fixture5204000053039865802BR6304ABCD",
|
|
45
|
+
"order_status": "pending"
|
|
46
|
+
},
|
|
47
|
+
"status_canceled": {
|
|
48
|
+
"checkout_session_id": "cks_def456",
|
|
49
|
+
"status": "canceled",
|
|
50
|
+
"error": "browser_worker_checkout_failed"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -34,6 +34,16 @@ def test_async_client_stores_project_id() -> None:
|
|
|
34
34
|
assert cs.project_id == "prj_0123456789abcdef"
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def test_async_client_rejects_bad_project_id_format() -> None:
|
|
38
|
+
with pytest.raises(ConfigError):
|
|
39
|
+
AsyncCodeSpar(api_key="csk_live_abc", project_id="not-a-project-id")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_sync_client_rejects_bad_project_id_format() -> None:
|
|
43
|
+
with pytest.raises(ConfigError):
|
|
44
|
+
CodeSpar(api_key="csk_live_abc", project_id="prj_tooshort")
|
|
45
|
+
|
|
46
|
+
|
|
37
47
|
def test_sync_client_requires_api_key() -> None:
|
|
38
48
|
with pytest.raises(ConfigError):
|
|
39
49
|
CodeSpar(api_key="")
|
|
@@ -87,14 +87,14 @@ async def test_session_level_project_id_wins(httpx_mock: HTTPXMock) -> None:
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
async with AsyncCodeSpar(
|
|
90
|
-
api_key="csk_test_x", project_id="
|
|
90
|
+
api_key="csk_test_x", project_id="prj_clientlevel00000"
|
|
91
91
|
) as cs:
|
|
92
|
-
await cs.create("user_123", preset="brazilian", project_id="
|
|
92
|
+
await cs.create("user_123", preset="brazilian", project_id="prj_sessionlevel0000")
|
|
93
93
|
|
|
94
94
|
request = httpx_mock.get_request()
|
|
95
95
|
assert request is not None
|
|
96
96
|
# session override wins
|
|
97
|
-
assert request.headers["x-codespar-project"] == "
|
|
97
|
+
assert request.headers["x-codespar-project"] == "prj_sessionlevel0000"
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
async def test_send_returns_tool_calls(httpx_mock: HTTPXMock) -> None:
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codespar_shop Python parity tests.
|
|
3
|
+
|
|
4
|
+
Two layers:
|
|
5
|
+
1. Round-trip: parse each canonical wire fixture
|
|
6
|
+
(tests/_fixtures/shop_canonical.json — mirrored from the TS
|
|
7
|
+
packages/types/src/testing/shop-fixtures.ts) into the dataclass
|
|
8
|
+
result and assert every field maps one-to-one. This is the
|
|
9
|
+
dict->dataclass parse-helper round-trip the contract requires
|
|
10
|
+
and the guard against TS<->Python wire drift.
|
|
11
|
+
2. Dispatch: drive ``session.shop(...)`` over a mocked backend and
|
|
12
|
+
assert the action-correct result type + the wire params the
|
|
13
|
+
facade sends.
|
|
14
|
+
|
|
15
|
+
The fixtures are the same JSON the TS conformance fixtures encode, so
|
|
16
|
+
both language surfaces are proven behaviorally identical for the same
|
|
17
|
+
wire payload.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import pytest
|
|
27
|
+
from pytest_httpx import HTTPXMock
|
|
28
|
+
|
|
29
|
+
from codespar import (
|
|
30
|
+
AsyncCodeSpar,
|
|
31
|
+
ShopArgs,
|
|
32
|
+
ShopCheckoutItem,
|
|
33
|
+
ShopCheckoutResult,
|
|
34
|
+
ShopSearchResult,
|
|
35
|
+
ShopStatusResult,
|
|
36
|
+
)
|
|
37
|
+
from codespar._async_session import _parse_shop_result
|
|
38
|
+
|
|
39
|
+
FIXTURE_PATH = Path(__file__).parent / "_fixtures" / "shop_canonical.json"
|
|
40
|
+
FIXTURES: dict[str, Any] = json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_search_fixture_round_trips() -> None:
|
|
44
|
+
result = _parse_shop_result(FIXTURES["search"], "search")
|
|
45
|
+
assert isinstance(result, ShopSearchResult)
|
|
46
|
+
assert result.rail == "vtex"
|
|
47
|
+
assert len(result.products) == 1
|
|
48
|
+
offer = result.products[0]
|
|
49
|
+
assert offer.product_id == "prod_1"
|
|
50
|
+
assert offer.sku_id == "sku_1"
|
|
51
|
+
assert offer.price_minor == 4990
|
|
52
|
+
assert offer.currency == "BRL"
|
|
53
|
+
assert offer.available is True
|
|
54
|
+
assert len(offer.variants) == 1
|
|
55
|
+
# The buyable SKU is on the variant — pass it as checkout variant_id.
|
|
56
|
+
assert offer.variants[0].sku_id == "sku_1"
|
|
57
|
+
assert offer.variants[0].available is True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_empty_search_is_success_not_error() -> None:
|
|
61
|
+
result = _parse_shop_result(FIXTURES["search_empty"], "search")
|
|
62
|
+
assert isinstance(result, ShopSearchResult)
|
|
63
|
+
assert result.products == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_checkout_fixture_round_trips() -> None:
|
|
67
|
+
result = _parse_shop_result(FIXTURES["checkout"], "checkout")
|
|
68
|
+
assert isinstance(result, ShopCheckoutResult)
|
|
69
|
+
assert result.checkout_session_id == "cks_abc123"
|
|
70
|
+
assert result.status == "in_progress"
|
|
71
|
+
assert result.message == "checkout started"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_status_ready_for_payment_exposes_typed_pix() -> None:
|
|
75
|
+
result = _parse_shop_result(
|
|
76
|
+
FIXTURES["status_ready_for_payment"], "checkout_status"
|
|
77
|
+
)
|
|
78
|
+
assert isinstance(result, ShopStatusResult)
|
|
79
|
+
assert result.status == "ready_for_payment"
|
|
80
|
+
assert result.total_minor == 4990
|
|
81
|
+
assert result.pix_copia_e_cola is not None
|
|
82
|
+
assert "br.gov.bcb.pix" in result.pix_copia_e_cola
|
|
83
|
+
assert result.order_status == "pending"
|
|
84
|
+
assert result.error is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_status_canceled_exposes_typed_error() -> None:
|
|
88
|
+
result = _parse_shop_result(
|
|
89
|
+
FIXTURES["status_canceled"], "checkout_status"
|
|
90
|
+
)
|
|
91
|
+
assert isinstance(result, ShopStatusResult)
|
|
92
|
+
assert result.status == "canceled"
|
|
93
|
+
assert result.error == "browser_worker_checkout_failed"
|
|
94
|
+
assert result.pix_copia_e_cola is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _session_json(**overrides: Any) -> dict[str, Any]:
|
|
98
|
+
return {
|
|
99
|
+
"id": "ses_shop",
|
|
100
|
+
"org_id": "org_test",
|
|
101
|
+
"user_id": "consumer_123",
|
|
102
|
+
"servers": ["cobasi"],
|
|
103
|
+
"status": "active",
|
|
104
|
+
"created_at": "2026-04-21T12:00:00Z",
|
|
105
|
+
"closed_at": None,
|
|
106
|
+
**overrides,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _execute_response_json(data: Any) -> dict[str, Any]:
|
|
111
|
+
return {
|
|
112
|
+
"success": True,
|
|
113
|
+
"data": data,
|
|
114
|
+
"error": None,
|
|
115
|
+
"duration": 5,
|
|
116
|
+
"server": "_codespar",
|
|
117
|
+
"tool": "codespar_shop",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_shop_search_dispatch(httpx_mock: HTTPXMock) -> None:
|
|
123
|
+
httpx_mock.add_response(
|
|
124
|
+
url="https://api.codespar.dev/v1/sessions",
|
|
125
|
+
method="POST",
|
|
126
|
+
json=_session_json(),
|
|
127
|
+
)
|
|
128
|
+
httpx_mock.add_response(
|
|
129
|
+
url="https://api.codespar.dev/v1/sessions/ses_shop/execute",
|
|
130
|
+
method="POST",
|
|
131
|
+
json=_execute_response_json(FIXTURES["search"]),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async with AsyncCodeSpar(api_key="csk_test_x") as cs:
|
|
135
|
+
session = await cs.create("consumer_123", preset="brazilian")
|
|
136
|
+
result = await session.shop(
|
|
137
|
+
ShopArgs(action="search", query="ração para gato", merchant="cobasi", limit=10)
|
|
138
|
+
)
|
|
139
|
+
assert isinstance(result, ShopSearchResult)
|
|
140
|
+
assert result.products[0].variants[0].sku_id == "sku_1"
|
|
141
|
+
|
|
142
|
+
# The facade serialized only the search fields.
|
|
143
|
+
requests = httpx_mock.get_requests()
|
|
144
|
+
exec_req = next(r for r in requests if r.url.path.endswith("/execute"))
|
|
145
|
+
body = json.loads(exec_req.content)
|
|
146
|
+
assert body["tool"] == "codespar_shop"
|
|
147
|
+
assert body["input"] == {
|
|
148
|
+
"action": "search",
|
|
149
|
+
"query": "ração para gato",
|
|
150
|
+
"limit": 10,
|
|
151
|
+
"merchant": "cobasi",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.asyncio
|
|
156
|
+
async def test_shop_checkout_serializes_items(httpx_mock: HTTPXMock) -> None:
|
|
157
|
+
httpx_mock.add_response(
|
|
158
|
+
url="https://api.codespar.dev/v1/sessions",
|
|
159
|
+
method="POST",
|
|
160
|
+
json=_session_json(),
|
|
161
|
+
)
|
|
162
|
+
httpx_mock.add_response(
|
|
163
|
+
url="https://api.codespar.dev/v1/sessions/ses_shop/execute",
|
|
164
|
+
method="POST",
|
|
165
|
+
json=_execute_response_json(FIXTURES["checkout"]),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async with AsyncCodeSpar(api_key="csk_test_x") as cs:
|
|
169
|
+
session = await cs.create("consumer_123", preset="brazilian")
|
|
170
|
+
result = await session.shop(
|
|
171
|
+
ShopArgs(
|
|
172
|
+
action="checkout",
|
|
173
|
+
merchant="cobasi",
|
|
174
|
+
items=[ShopCheckoutItem(variant_id="sku_1", quantity=1)],
|
|
175
|
+
consumer_id="consumer_123",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
assert isinstance(result, ShopCheckoutResult)
|
|
179
|
+
assert result.status == "in_progress"
|
|
180
|
+
|
|
181
|
+
requests = httpx_mock.get_requests()
|
|
182
|
+
exec_req = next(r for r in requests if r.url.path.endswith("/execute"))
|
|
183
|
+
body = json.loads(exec_req.content)
|
|
184
|
+
assert body["input"]["action"] == "checkout"
|
|
185
|
+
assert body["input"]["items"] == [{"variant_id": "sku_1", "quantity": 1}]
|
|
186
|
+
assert body["input"]["consumer_id"] == "consumer_123"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.asyncio
|
|
190
|
+
async def test_shop_failure_raises(httpx_mock: HTTPXMock) -> None:
|
|
191
|
+
httpx_mock.add_response(
|
|
192
|
+
url="https://api.codespar.dev/v1/sessions",
|
|
193
|
+
method="POST",
|
|
194
|
+
json=_session_json(),
|
|
195
|
+
)
|
|
196
|
+
httpx_mock.add_response(
|
|
197
|
+
url="https://api.codespar.dev/v1/sessions/ses_shop/execute",
|
|
198
|
+
method="POST",
|
|
199
|
+
json={
|
|
200
|
+
"success": False,
|
|
201
|
+
"data": None,
|
|
202
|
+
"error": "invalid_args",
|
|
203
|
+
"duration": 1,
|
|
204
|
+
"server": "_codespar",
|
|
205
|
+
"tool": "codespar_shop",
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async with AsyncCodeSpar(api_key="csk_test_x") as cs:
|
|
210
|
+
session = await cs.create("consumer_123", preset="brazilian")
|
|
211
|
+
with pytest.raises(Exception, match="shop failed: invalid_args"):
|
|
212
|
+
await session.shop(ShopArgs(action="search", query="x"))
|
|
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
|