walmart-support 0.1.0__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.
Files changed (33) hide show
  1. walmart_support-0.1.0/.claude-plugin/marketplace.json +16 -0
  2. walmart_support-0.1.0/.claude-plugin/plugin.json +9 -0
  3. walmart_support-0.1.0/.gitattributes +3 -0
  4. walmart_support-0.1.0/.github/workflows/ci.yml +69 -0
  5. walmart_support-0.1.0/.gitignore +19 -0
  6. walmart_support-0.1.0/.python-version +1 -0
  7. walmart_support-0.1.0/AGENTS.md +117 -0
  8. walmart_support-0.1.0/CLAUDE.md +1 -0
  9. walmart_support-0.1.0/LICENSE +21 -0
  10. walmart_support-0.1.0/PKG-INFO +259 -0
  11. walmart_support-0.1.0/README.md +250 -0
  12. walmart_support-0.1.0/config.example.json +6 -0
  13. walmart_support-0.1.0/pyproject.toml +43 -0
  14. walmart_support-0.1.0/scripts/cleanup_probe_attachment.py +43 -0
  15. walmart_support-0.1.0/skills/walmart-support/SKILL.md +89 -0
  16. walmart_support-0.1.0/src/walmart_support/__init__.py +0 -0
  17. walmart_support-0.1.0/src/walmart_support/attachments.py +102 -0
  18. walmart_support-0.1.0/src/walmart_support/aura.py +267 -0
  19. walmart_support-0.1.0/src/walmart_support/auth.py +260 -0
  20. walmart_support-0.1.0/src/walmart_support/cases.py +331 -0
  21. walmart_support-0.1.0/src/walmart_support/cli.py +513 -0
  22. walmart_support-0.1.0/src/walmart_support/config.py +67 -0
  23. walmart_support-0.1.0/src/walmart_support/create.py +284 -0
  24. walmart_support-0.1.0/tests/__init__.py +0 -0
  25. walmart_support-0.1.0/tests/conftest.py +74 -0
  26. walmart_support-0.1.0/tests/test_attachments.py +101 -0
  27. walmart_support-0.1.0/tests/test_aura.py +132 -0
  28. walmart_support-0.1.0/tests/test_auth.py +82 -0
  29. walmart_support-0.1.0/tests/test_cases.py +342 -0
  30. walmart_support-0.1.0/tests/test_config.py +53 -0
  31. walmart_support-0.1.0/tests/test_create.py +221 -0
  32. walmart_support-0.1.0/tests/test_session.py +149 -0
  33. walmart_support-0.1.0/uv.lock +230 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "walmart-support",
3
+ "description": "The walmart-support CLI and the skill that drives it.",
4
+ "owner": {
5
+ "name": "alyiox"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "walmart-support",
10
+ "source": "./",
11
+ "description": "Read, answer, and file Walmart Connect advertising support cases from the shell.",
12
+ "category": "productivity",
13
+ "homepage": "https://github.com/alyiox/walmart-support"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "walmart-support",
3
+ "description": "Read, answer, and file Walmart Connect advertising support cases from the shell, using the walmart-support CLI.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "alyiox"
7
+ },
8
+ "homepage": "https://github.com/alyiox/walmart-support"
9
+ }
@@ -0,0 +1,3 @@
1
+ * text=auto eol=lf
2
+ *.{cmd,[cC][mM][dD]} text eol=crlf
3
+ *.{bat,[bB][aA][tT]} text eol=crlf
@@ -0,0 +1,69 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["*"]
7
+ pull_request:
8
+ branches: [main]
9
+ workflow_dispatch:
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ test:
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v7
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
28
+ with:
29
+ enable-cache: true
30
+
31
+ - name: Install dependencies
32
+ run: uv sync --group dev
33
+
34
+ - name: Lint and Format
35
+ run: |
36
+ uv run ruff check src/ tests/
37
+ uv run ruff format --check src/ tests/
38
+
39
+ - name: Type check
40
+ run: uv run pyright
41
+
42
+ - name: Test
43
+ run: uv run pytest tests/ -v --tb=short
44
+
45
+ publish:
46
+ name: Release to PyPI
47
+ needs: test
48
+ if: startsWith(github.ref, 'refs/tags/')
49
+ runs-on: ubuntu-latest
50
+ environment:
51
+ name: pypi
52
+ url: https://pypi.org/project/walmart-support/${{ github.ref_name }}/
53
+ permissions:
54
+ contents: read
55
+ id-token: write
56
+ steps:
57
+ - name: Check out repository
58
+ uses: actions/checkout@v7
59
+
60
+ - name: Install uv package manager
61
+ uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
62
+ with:
63
+ enable-cache: true
64
+
65
+ - name: Build Python package distributions
66
+ run: uv build
67
+
68
+ - name: Publish Python package to PyPI
69
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,19 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Local dev notes
13
+ *-progress.md
14
+
15
+ # Local secrets
16
+ config.json
17
+ *.pem
18
+ smoke.curl.txt
19
+ *.curl.txt
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,117 @@
1
+ # Agent Instructions
2
+
3
+ Rules AI agents must follow when working in this repository.
4
+
5
+ ---
6
+
7
+ ## Commit messages
8
+
9
+ Use **Conventional Commits**.
10
+
11
+ ### Header
12
+
13
+ * Format: `<type>(optional scope): summary`
14
+ * Use lowercase types (`feat`, `fix`, `ci`, `chore`, `docs`)
15
+ * Use scopes when relevant
16
+ * Write summaries in lowercase, imperative mood
17
+
18
+ ### Body
19
+
20
+ * Leave a blank line after the header
21
+ * Explain **why**, not what
22
+ * Use imperative, present tense
23
+ * Wrap lines at ~72 characters
24
+
25
+ The body is optional for trivial changes.
26
+
27
+ ---
28
+
29
+ ## Release tags
30
+
31
+ * Use the bare version as the tag name — **no `v` prefix** (e.g. `0.1.1`, not `v0.1.1`)
32
+ * Prefer final versions over pre-releases: a resolver skips pre-releases unless
33
+ asked, so `uvx walmart-support` cannot see an `a`/`b`/`rc` build
34
+ * Tags must be annotated (`git tag -a`) with a structured release-notes message
35
+
36
+ ---
37
+
38
+ ## Commits
39
+
40
+ When generating commits via a shell:
41
+
42
+ * Do **not** pass generated messages directly to `git commit -m`
43
+ * Write the commit message to a file or standard input
44
+ * Use `git commit -F <file>` or `git commit -F -`
45
+ * Disable shell expansion when writing commit messages
46
+
47
+ This avoids issues with backticks, quotes, and other shell-expanded
48
+ characters in generated commit messages.
49
+
50
+ ---
51
+
52
+ ## Code style
53
+
54
+ Follow existing project conventions.
55
+
56
+ * Match formatting, naming, and file structure already in use
57
+ * Do not reformat unrelated code
58
+ * Prefer small, focused changes
59
+ * Avoid introducing new patterns without clear benefit
60
+
61
+ ### Language-specific rules
62
+
63
+ * Respect `.editorconfig` when present
64
+ * Do not disable lint rules without justification
65
+ * Prefer explicit, readable code over clever abstractions
66
+ * Ensure all changes pass `ruff check .`, `ruff format --check .`, `pyright`, and `pytest`
67
+
68
+ ---
69
+
70
+ ## `uv` Workflow Rules
71
+
72
+ * Use `uv` exclusively for dependency management instead of `pip`
73
+ * Always prefix tool and script invocations with `uv run` so they execute inside the managed environment
74
+ * Do not manually create, activate, or delete `.venv` directories
75
+ * Use `uv version <new-version>` to bump the project version — do **not** edit `pyproject.toml` directly
76
+ * Always commit both `pyproject.toml` and `uv.lock` together after a version bump
77
+
78
+ ---
79
+
80
+ ## Attribution
81
+
82
+ All commits must include an `Assisted-by` trailer line:
83
+
84
+ ```
85
+ Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]
86
+ ```
87
+
88
+ * **AGENT_NAME** — the AI tool or framework used (e.g. `Claude`, `Cursor`, `Copilot`)
89
+ * **MODEL_VERSION** — the specific model version (e.g. `claude-opus-4-6`)
90
+ * **[TOOL1] [TOOL2]** — optional, space-separated list of specialized analysis tools used in the change (e.g. `coccinelle`, `sparse`, `smatch`, `clang-tidy`)
91
+ * Do **not** list everyday tools like `git`, `gcc`, `make`, or editors
92
+
93
+ Example:
94
+
95
+ ```
96
+ Assisted-by: Claude:claude-opus-4-6 coccinelle sparse
97
+ ```
98
+
99
+ ---
100
+
101
+ ## CLI conventions
102
+
103
+ The command surface is the product. Keep it predictable.
104
+
105
+ * Group commands by the noun they act on (`cases`, `categories`, `auth`); the
106
+ group name carries the entity, the subcommand carries the verb
107
+ * Every command MUST accept `--json` and emit machine-readable output under it,
108
+ with the human rendering as the default
109
+ * Take prose (case bodies, replies) through a `--*-file` flag rather than an
110
+ argument, so long text never passes through shell quoting
111
+ * A command that changes state at Walmart's end MUST NOT act on defaults alone:
112
+ `cases create` prints its payload unless `--submit` is given
113
+ * Exit codes: `0` success, `1` portal or network failure, `2` usage, config, or
114
+ refused input
115
+ * A change to a flag's behaviour or cost updates `skills/walmart-support/SKILL.md`
116
+ in the same commit: the skill documents the gates and per-case request costs
117
+ that the help text does not
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jon X
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.5
2
+ Name: walmart-support
3
+ Version: 0.1.0
4
+ Summary: CLI for Walmart Connect advertising support cases
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: httpx>=0.27.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Walmart Connect Advertising Support Cases
11
+
12
+ [![CI](https://github.com/alyiox/walmart-support/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/alyiox/walmart-support/actions/workflows/ci.yml)
13
+ [![PyPI](https://img.shields.io/pypi/v/walmart-support.svg)](https://pypi.org/project/walmart-support/)
14
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg)](https://www.python.org/downloads/)
15
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
16
+
17
+ CLI for reading and filing [Walmart Connect](https://advertisinghelp.walmart.com) advertising
18
+ support cases without driving a browser.
19
+
20
+ The Advertising Help portal is a Salesforce Experience Cloud site with no public API. Every click in
21
+ its UI is one POST to `/s/sfsites/aura` naming an `@AuraEnabled` Apex method, so the flow is
22
+ scriptable — which matters, because submitting a case body through a headless browser is slow and,
23
+ in some environments, unreliable enough to leave you unsure whether a case was actually filed.
24
+
25
+ ## Status
26
+
27
+ Working end to end: authentication, listing and reading cases, replying, attaching files, closing,
28
+ and filing new cases. Each Aura payload was captured from the portal UI and verified against a real
29
+ case, so treat behaviour outside the documented commands as unmapped rather than unsupported.
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.13+, or [uv](https://docs.astral.sh/uv/)
34
+ - A Walmart Advertising Help portal account (the Partners → Help Site login)
35
+
36
+ ## Quick start
37
+
38
+ There is nothing to install — `uvx` fetches and runs it:
39
+
40
+ ```bash
41
+ mkdir -p ~/.config/walmart-support
42
+ cp config.example.json ~/.config/walmart-support/config.json
43
+ $EDITOR ~/.config/walmart-support/config.json
44
+
45
+ uvx walmart-support auth check
46
+ uvx walmart-support cases list --status "need info"
47
+ uvx walmart-support cases get 15957474
48
+ uvx walmart-support cases replies 15957474 --from-walmart
49
+
50
+ uvx walmart-support cases create \
51
+ --platform display \
52
+ --category "API Support" --issue "Endpoint-specific problem" \
53
+ --subject "Display API: ..." --advertisers "241727, 244985" \
54
+ --description-file ./body.txt # prints the payload
55
+ uvx walmart-support cases create ... --submit # actually files it
56
+
57
+ # where the --category and --issue names come from
58
+ uvx walmart-support categories list --platform sponsored-search
59
+ ```
60
+
61
+ Reaching for it daily, or working offline? `uv tool install walmart-support`
62
+ puts it on `PATH` and starts faster; `walmart-support --version` reports which
63
+ build you are on either way. Inside a clone of this repo, use `uv run
64
+ walmart-support ...` to exercise your working tree.
65
+
66
+ ## Replying and attaching
67
+
68
+ `cases reply` posts a comment on an existing case, and `cases attach` uploads
69
+ files to one:
70
+
71
+ ```bash
72
+ walmart-support cases reply 15957474 --message-file answer.txt
73
+ walmart-support cases attach 15957474 ./har.json ./adgroup.json
74
+ ```
75
+
76
+ Neither is gated behind a confirmation flag, unlike `cases create`: they act on
77
+ a case you already own, with no category mapping to get wrong. `create` files a
78
+ *new* record into the support queue, which is the thing worth a second look.
79
+
80
+ Both are writes, so **do not wrap them in a retry loop.** A network error after
81
+ the write lands looks identical to one before it, and retrying uploads the file
82
+ or posts the comment twice — the portal has no idempotency key.
83
+
84
+ Deletion is asymmetric and this bites: an upload returns a **ContentVersion**
85
+ id (`068…`), while the portal's delete actions want the **ContentDocument** id
86
+ (`069…`) and silently do nothing when handed the other. The `069` ids are
87
+ recoverable from a case's detail payload; `attachments.document_ids()` extracts
88
+ them.
89
+
90
+ Attaching *while filing* is not supported yet: `openCase` accepts a
91
+ `documentId` list, but `saveChunk` requires a `parentId` and the case does not
92
+ exist yet, so it is unclear what the portal passes at that point.
93
+
94
+ ## Filing a case
95
+
96
+ `cases create` prints the exact `openCase` payload and files nothing unless
97
+ `--submit` is given. That default is deliberate: a case goes to Walmart's
98
+ support queue, and a mis-mapped category files a real but misrouted one.
99
+
100
+ Categories are resolved by name against the portal's own dropdown data rather
101
+ than hardcoded, so `categories list` shows exactly what the UI offers and both
102
+ the UI label ("API Support") and the wizard's internal name ("API") match.
103
+
104
+ `--platform` covers Display and Sponsored Search; the portal collapses Search
105
+ onto its "Sponsored Products" ad unit, which is why a Search case shows
106
+ Sponsored Products as its platform. Sponsored Brands and Videos are accepted
107
+ but the portal publishes no categories for them on a partner account, and the
108
+ error says so.
109
+
110
+ `openCase` returns the new case's number, and the whole path is verified
111
+ end to end — filed, replied to and closed. Three things had to be right, none
112
+ of which the method signature revealed:
113
+
114
+ * `additionalFieldsString` is a JSON **list** of `{title, value}` objects, not
115
+ an object. Apex deserializes it into a `List` and names any unexpected key.
116
+ * `partnershipType`/`partnershipId` must be **empty**. `Partnership__c` is a
117
+ lookup to a Partnership record for supplier and seller channels, so an
118
+ account id there fails the insert with `FIELD_INTEGRITY_EXCEPTION`.
119
+ * Category routing comes from the resolved level-1/level-2 pair, and a filed
120
+ case reports `API-AdCases` / `Endpoint-specific problem` back.
121
+
122
+ > **Known defect:** the portal resolves additional fields loosely against the
123
+ > `title` we send. On the first CLI-filed case, `Advertiser Account Name` and
124
+ > `Advertisers Affected` collided on their shared prefix and the account name
125
+ > was stored under the advertisers field, dropping the ids. The colliding field
126
+ > is no longer sent, which should fix it, but that is **unconfirmed until the
127
+ > next filing** — and the wrapper evidently carries an identifier beyond
128
+ > `title`/`value` (its deserializer also accepts `fieldName`).
129
+ >
130
+ > Either way, put anything that matters in the description body: it is stored
131
+ > verbatim, whereas these fields are not reliably addressable.
132
+
133
+ ### Closing and replying
134
+
135
+ ```bash
136
+ walmart-support cases reply 15971149 --message-file answer.txt
137
+ walmart-support cases close 15971149 # New -> Closed
138
+ ```
139
+
140
+ ## Sessions
141
+
142
+ Each invocation is its own process, so session cookies are cached in
143
+ `$XDG_CACHE_HOME/walmart-support/session.json` (mode `0600`) and reused
144
+ until the portal rejects them. Without that, every command would pay a full
145
+ login — four requests and a Salesforce login event before doing any work; with
146
+ it, a warm command is roughly twice as fast and logs in only when it must.
147
+
148
+ A session that dies mid-command is retried once from a clean login, because
149
+ Salesforce reports an invalid session in the middle of a request rather than up
150
+ front. `walmart-support auth logout` discards the cached session.
151
+
152
+ ## Configuration
153
+
154
+ `~/.config/walmart-support/config.json`:
155
+
156
+ | Key | Required | Description |
157
+ | --- | --- | --- |
158
+ | `base_url` | no | Portal origin. Defaults to `https://advertisinghelp.walmart.com`. |
159
+ | `username` | yes | Portal login email. |
160
+ | `password` | yes | Portal password. |
161
+ | `timeout` | no | Per-request timeout in seconds (default 60). |
162
+
163
+ Every key can be overridden by an environment variable — `WALMART_SUPPORT_USERNAME`,
164
+ `WALMART_SUPPORT_PASSWORD`, `WALMART_SUPPORT_BASE_URL`, `WALMART_SUPPORT_TIMEOUT` — so CI needs no
165
+ file on disk.
166
+
167
+ Credentials are the only way in, deliberately. A session cookie cannot be configured by hand:
168
+ Salesforce `sid` cookies are session-scoped, expire on their own, and cannot renew themselves, so a
169
+ configured one becomes a stale secret that fails in a way the tool can do nothing about. Sessions
170
+ are managed by the cache below instead.
171
+
172
+ ## Reading a case
173
+
174
+ `cases list` is backed by one action that returns every case at once, but it
175
+ **abbreviates** `subject` and `description` — the trailing `...` comes from
176
+ Walmart. `cases get` uses the detail action instead, which returns the full text
177
+ plus status, priority, category and the submitted form fields.
178
+
179
+ ### Searching
180
+
181
+ `cases list --query` filters on the text the list action returns — and that text
182
+ is **abbreviated**: subjects are cut to roughly 46 characters, and some cases
183
+ carry almost no description at all. So a plain `--query` can miss a case whose
184
+ real body contains the term.
185
+
186
+ `--deep` re-reads each candidate's full text *and its replies* instead, at one
187
+ request per candidate:
188
+
189
+ ```bash
190
+ # finds nothing: the term sits past where the list truncates the subject
191
+ walmart-support cases list --query "targeting of a LIVE ad group"
192
+
193
+ # finds both cases
194
+ walmart-support cases list --query "targeting of a LIVE ad group" --since 2026-08-01 --deep
195
+ ```
196
+
197
+ Because it fans out, `--deep` refuses to run on more candidates than its cap
198
+ (25) and asks you to narrow with `--status`/`--since`/`--limit` rather than
199
+ firing a request per case in the account.
200
+
201
+ `--limit` on its own is handed to the portal as a SOQL `LIMIT`. Combined with a
202
+ filter it stays client-side, since the server would otherwise apply it *before*
203
+ filtering and return matches from an arbitrary slice.
204
+
205
+ `cases replies` shows the case conversation, flattened from the HTML the portal
206
+ stores, with each message attributed to `us` or `WALMART`:
207
+
208
+ ```bash
209
+ walmart-support cases replies 15957474 # whole thread
210
+ walmart-support cases replies 15957474 --from-walmart # skip our own posts
211
+ walmart-support cases replies 15957474 --latest 1 # just the newest
212
+ ```
213
+
214
+ Support's acknowledgement mails quote the entire case body back, and later
215
+ replies quote the ones before them, so an unfiltered thread is mostly repetition
216
+ of what you already sent — `--from-walmart --latest 1` is usually what you want.
217
+
218
+ ## Claude Code plugin
219
+
220
+ The repo doubles as a [Claude Code](https://claude.com/claude-code) plugin, so the workflow
221
+ knowledge travels with the CLI instead of living in one person's `~/.claude`:
222
+
223
+ ```
224
+ /plugin marketplace add alyiox/walmart-support
225
+ /plugin install walmart-support@walmart-support
226
+ ```
227
+
228
+ That installs a skill covering the parts the `--help` output cannot express — that `--deep` costs
229
+ one request per case, that support's replies quote the whole thread back, that `cases create` files
230
+ nothing without `--submit`, and that `cases close` is one-way. The plugin does not install the CLI;
231
+ `walmart-support --version` tells you whether you still need to.
232
+
233
+ ## How it works
234
+
235
+ ```
236
+ GET /s/contact scrape the Aura context (fwuid, apck, lrmc, markup hash)
237
+ POST /s/sfsites/aura?r=N&...login authenticate via LightningLoginFormController
238
+ POST /s/sfsites/aura?r=N&other.... call @AuraEnabled Apex methods through ApexActionController
239
+ ```
240
+
241
+ Aura rejects any call whose framework context does not match the deployed build, and that context
242
+ rotates with every Salesforce release, so it is scraped on each run and never hardcoded. All of this
243
+ lives in `aura.py`; when Walmart's contract shifts, that is the one module to re-capture against.
244
+
245
+ ## Development
246
+
247
+ ```bash
248
+ uv sync --group dev
249
+ uv run ruff check src/ tests/
250
+ uv run ruff format --check src/ tests/
251
+ uv run pyright
252
+ uv run pytest tests/ -v
253
+ ```
254
+
255
+ Tests are offline: they exercise recorded page shapes and Aura envelopes through `httpx.MockTransport`.
256
+
257
+ ## License
258
+
259
+ MIT