tradera-cli 0.1.0__tar.gz → 0.1.1__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 (22) hide show
  1. {tradera_cli-0.1.0/tradera_cli.egg-info → tradera_cli-0.1.1}/PKG-INFO +14 -1
  2. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/README.md +13 -0
  3. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/pyproject.toml +1 -1
  4. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tests/test_api.py +198 -0
  5. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tests/test_cli.py +16 -0
  6. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli/__init__.py +1 -1
  7. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli/api.py +77 -1
  8. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli/cli.py +16 -0
  9. {tradera_cli-0.1.0 → tradera_cli-0.1.1/tradera_cli.egg-info}/PKG-INFO +14 -1
  10. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/LICENSE +0 -0
  11. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/setup.cfg +0 -0
  12. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tests/test_cli_entry.py +0 -0
  13. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tests/test_formatters.py +0 -0
  14. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tests/test_import.py +0 -0
  15. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tests/test_main_module.py +0 -0
  16. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli/__main__.py +0 -0
  17. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli/formatters.py +0 -0
  18. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli.egg-info/SOURCES.txt +0 -0
  19. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli.egg-info/dependency_links.txt +0 -0
  20. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli.egg-info/entry_points.txt +0 -0
  21. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli.egg-info/requires.txt +0 -0
  22. {tradera_cli-0.1.0 → tradera_cli-0.1.1}/tradera_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradera-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: CLI for Tradera web endpoints
5
5
  Author: Johnny W
6
6
  License-Expression: MIT
@@ -69,6 +69,11 @@ tradera search "iphone"
69
69
  tradera search "pokemon" --page 2 --page-size 20
70
70
  tradera search "kamera" --sort AddedOn --format json
71
71
  tradera search "klocka" --country SE --lang sv --format jsonl
72
+ tradera search "lego" --item-status Sold --format json
73
+ tradera search "lego" --condition "Oanvänt" --format json
74
+ tradera search "lego" --item-type FixedPrice --from-price 100 --to-price 500
75
+ tradera search "lego" --allowed-buyer-regions eu --counties Stockholm Uppsala
76
+ tradera search "lego city" --search-type ExactSearch
72
77
  ```
73
78
 
74
79
  ### Get item details
@@ -105,6 +110,14 @@ Search command (`tradera search`) supports:
105
110
  | `--sort` | Sort mode (default: `Relevance`) |
106
111
  | `--lang` | Language code (default: `sv`) |
107
112
  | `--country` | Shipping country code (default: `SE`) |
113
+ | `--item-status` | Filter by item status: `Active`, `Sold`, `Unsold` |
114
+ | `--condition` | Filter by condition: `Oanvänt`, `Mycket gott skick`, `Gott skick`, `Okej skick`, `Defekt` |
115
+ | `--item-type` | Filter by listing type: `All`, `Auction`, `FixedPrice`, `ContactOnly` |
116
+ | `--from-price` | Minimum price |
117
+ | `--to-price` | Maximum price |
118
+ | `--allowed-buyer-regions` | Buyer region filter: `sweden`, `eu`, `international` |
119
+ | `--counties` | Pickup counties, passed as one or more values, for example `Stockholm Uppsala` |
120
+ | `--search-type` | Search mode. Current supported value: `ExactSearch` |
108
121
  | `--no-translate` | Disable automatic translation preference |
109
122
  | `--format` | Output format: `table`, `json`, `jsonl` |
110
123
 
@@ -39,6 +39,11 @@ tradera search "iphone"
39
39
  tradera search "pokemon" --page 2 --page-size 20
40
40
  tradera search "kamera" --sort AddedOn --format json
41
41
  tradera search "klocka" --country SE --lang sv --format jsonl
42
+ tradera search "lego" --item-status Sold --format json
43
+ tradera search "lego" --condition "Oanvänt" --format json
44
+ tradera search "lego" --item-type FixedPrice --from-price 100 --to-price 500
45
+ tradera search "lego" --allowed-buyer-regions eu --counties Stockholm Uppsala
46
+ tradera search "lego city" --search-type ExactSearch
42
47
  ```
43
48
 
44
49
  ### Get item details
@@ -75,6 +80,14 @@ Search command (`tradera search`) supports:
75
80
  | `--sort` | Sort mode (default: `Relevance`) |
76
81
  | `--lang` | Language code (default: `sv`) |
77
82
  | `--country` | Shipping country code (default: `SE`) |
83
+ | `--item-status` | Filter by item status: `Active`, `Sold`, `Unsold` |
84
+ | `--condition` | Filter by condition: `Oanvänt`, `Mycket gott skick`, `Gott skick`, `Okej skick`, `Defekt` |
85
+ | `--item-type` | Filter by listing type: `All`, `Auction`, `FixedPrice`, `ContactOnly` |
86
+ | `--from-price` | Minimum price |
87
+ | `--to-price` | Maximum price |
88
+ | `--allowed-buyer-regions` | Buyer region filter: `sweden`, `eu`, `international` |
89
+ | `--counties` | Pickup counties, passed as one or more values, for example `Stockholm Uppsala` |
90
+ | `--search-type` | Search mode. Current supported value: `ExactSearch` |
78
91
  | `--no-translate` | Disable automatic translation preference |
79
92
  | `--format` | Output format: `table`, `json`, `jsonl` |
80
93
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tradera-cli"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "CLI for Tradera web endpoints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ from urllib.parse import parse_qs, urlparse
5
+
3
6
  import requests
4
7
  import pytest
5
8
 
@@ -212,6 +215,201 @@ def test_search_builds_payload() -> None:
212
215
  assert payload["sortBy"] == "EndDate"
213
216
 
214
217
 
218
+ def test_search_uses_search_page_for_sold_items() -> None:
219
+ client = TraderaClient()
220
+ captured: dict[str, object] = {}
221
+ next_data = {
222
+ "props": {
223
+ "pageProps": {
224
+ "initialState": {
225
+ "discover": {
226
+ "items": [{"itemId": 42, "isActive": False}],
227
+ "filters": {"itemStatus": {"selectedValue": "Sold"}},
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ def fake_request(method: str, path: str, **_kwargs: object) -> str:
235
+ captured["method"] = method
236
+ captured["path"] = path
237
+ return (
238
+ '<script id="__NEXT_DATA__" type="application/json">'
239
+ f"{json.dumps(next_data)}"
240
+ "</script>"
241
+ )
242
+
243
+ client._request = fake_request # type: ignore[method-assign]
244
+
245
+ result = client.search("kamera", page=3, sort_by="EndDate", item_status="Sold")
246
+
247
+ assert result["items"][0]["itemId"] == 42
248
+ assert captured["method"] == "GET"
249
+ parsed = urlparse(str(captured["path"]))
250
+ assert parsed.path == "/search"
251
+ assert parse_qs(parsed.query) == {
252
+ "q": ["kamera"],
253
+ "paging": ["3"],
254
+ "sortBy": ["EndDate"],
255
+ "languageCodeIso2": ["sv"],
256
+ "itemStatus": ["Sold"],
257
+ }
258
+
259
+
260
+ def test_search_uses_search_page_for_condition_filter() -> None:
261
+ client = TraderaClient()
262
+ captured: dict[str, object] = {}
263
+ next_data = {
264
+ "props": {
265
+ "pageProps": {
266
+ "initialState": {
267
+ "discover": {
268
+ "items": [{"itemId": 99}],
269
+ "attributeFilters": [{"parameter": "af-condition", "selectedValues": ["Oanvänt"]}],
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ def fake_request(method: str, path: str, **_kwargs: object) -> str:
277
+ captured["method"] = method
278
+ captured["path"] = path
279
+ return (
280
+ '<script id="__NEXT_DATA__" type="application/json">'
281
+ f"{json.dumps(next_data, ensure_ascii=False)}"
282
+ "</script>"
283
+ )
284
+
285
+ client._request = fake_request # type: ignore[method-assign]
286
+
287
+ result = client.search("kamera", condition="Oanvänt")
288
+
289
+ assert result["items"][0]["itemId"] == 99
290
+ assert captured["method"] == "GET"
291
+ parsed = urlparse(str(captured["path"]))
292
+ assert parsed.path == "/search"
293
+ assert parse_qs(parsed.query) == {
294
+ "q": ["kamera"],
295
+ "paging": ["1"],
296
+ "sortBy": ["Relevance"],
297
+ "languageCodeIso2": ["sv"],
298
+ "af-condition": ["Oanvänt"],
299
+ }
300
+
301
+
302
+ def test_search_uses_search_page_for_additional_page_filters() -> None:
303
+ client = TraderaClient()
304
+ captured: dict[str, object] = {}
305
+ next_data = {
306
+ "props": {
307
+ "pageProps": {
308
+ "initialState": {
309
+ "discover": {
310
+ "items": [{"itemId": 77}],
311
+ "filters": {
312
+ "itemType": {"selectedValue": "FixedPrice"},
313
+ "allowedBuyerRegions": {"selectedValue": "eu;international"},
314
+ },
315
+ "filterPrice": {
316
+ "fromPrice": {"parameter": "fromPrice", "value": 100},
317
+ "toPrice": {"parameter": "toPrice", "value": 500},
318
+ },
319
+ "filterCounties": {"selectedValues": ["Stockholm", "Uppsala"]},
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ def fake_request(method: str, path: str, **_kwargs: object) -> str:
327
+ captured["method"] = method
328
+ captured["path"] = path
329
+ return (
330
+ '<script id="__NEXT_DATA__" type="application/json">'
331
+ f"{json.dumps(next_data, ensure_ascii=False)}"
332
+ "</script>"
333
+ )
334
+
335
+ client._request = fake_request # type: ignore[method-assign]
336
+
337
+ result = client.search(
338
+ "kamera",
339
+ item_type="FixedPrice",
340
+ from_price=100,
341
+ to_price=500,
342
+ allowed_buyer_regions="eu",
343
+ counties=["Stockholm", "Uppsala"],
344
+ )
345
+
346
+ assert result["items"][0]["itemId"] == 77
347
+ assert captured["method"] == "GET"
348
+ parsed = urlparse(str(captured["path"]))
349
+ assert parsed.path == "/search"
350
+ assert parse_qs(parsed.query) == {
351
+ "q": ["kamera"],
352
+ "paging": ["1"],
353
+ "sortBy": ["Relevance"],
354
+ "languageCodeIso2": ["sv"],
355
+ "itemType": ["FixedPrice"],
356
+ "fromPrice": ["100"],
357
+ "toPrice": ["500"],
358
+ "allowedBuyerRegions": ["eu;international"],
359
+ "counties": ["Stockholm;Uppsala"],
360
+ }
361
+
362
+
363
+ def test_search_uses_search_page_for_search_type() -> None:
364
+ client = TraderaClient()
365
+ captured: dict[str, object] = {}
366
+ next_data = {
367
+ "props": {
368
+ "pageProps": {
369
+ "initialState": {
370
+ "discover": {
371
+ "items": [{"itemId": 88}],
372
+ "queryParams": {"q": "kamera", "searchType": "ExactSearch"},
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ def fake_request(method: str, path: str, **_kwargs: object) -> str:
380
+ captured["method"] = method
381
+ captured["path"] = path
382
+ return (
383
+ '<script id="__NEXT_DATA__" type="application/json">'
384
+ f"{json.dumps(next_data)}"
385
+ "</script>"
386
+ )
387
+
388
+ client._request = fake_request # type: ignore[method-assign]
389
+
390
+ result = client.search("kamera", search_type="ExactSearch")
391
+
392
+ assert result["items"][0]["itemId"] == 88
393
+ assert captured["method"] == "GET"
394
+ parsed = urlparse(str(captured["path"]))
395
+ assert parsed.path == "/search"
396
+ assert parse_qs(parsed.query) == {
397
+ "q": ["kamera"],
398
+ "paging": ["1"],
399
+ "sortBy": ["Relevance"],
400
+ "languageCodeIso2": ["sv"],
401
+ "searchType": ["ExactSearch"],
402
+ }
403
+
404
+
405
+ def test_search_page_raises_when_next_data_is_missing() -> None:
406
+ client = TraderaClient()
407
+ client._request = lambda *_args, **_kwargs: "<html></html>" # type: ignore[method-assign]
408
+
409
+ with pytest.raises(TraderaApiError, match="Could not parse search page response"):
410
+ client.search("kamera", item_status="Unsold")
411
+
412
+
215
413
  def test_item_returns_dict_or_raises() -> None:
216
414
  client = TraderaClient()
217
415
  client._request = lambda *_args, **_kwargs: {"itemId": 123} # type: ignore[method-assign]
@@ -64,6 +64,14 @@ def test_cmd_search_and_no_translate(monkeypatch: pytest.MonkeyPatch, capsys: py
64
64
  sort="Relevance",
65
65
  lang="sv",
66
66
  country="SE",
67
+ item_status="Sold",
68
+ condition="Oanvänt",
69
+ item_type="FixedPrice",
70
+ from_price=100,
71
+ to_price=500,
72
+ allowed_buyer_regions="eu",
73
+ counties=["Stockholm", "Uppsala"],
74
+ search_type="ExactSearch",
67
75
  no_translate=True,
68
76
  format="table",
69
77
  )
@@ -72,6 +80,14 @@ def test_cmd_search_and_no_translate(monkeypatch: pytest.MonkeyPatch, capsys: py
72
80
 
73
81
  assert code == 0
74
82
  assert calls["automatic_translation_preferred"] is False
83
+ assert calls["item_status"] == "Sold"
84
+ assert calls["condition"] == "Oanvänt"
85
+ assert calls["item_type"] == "FixedPrice"
86
+ assert calls["from_price"] == 100
87
+ assert calls["to_price"] == 500
88
+ assert calls["allowed_buyer_regions"] == "eu"
89
+ assert calls["counties"] == ["Stockholm", "Uppsala"]
90
+ assert calls["search_type"] == "ExactSearch"
75
91
  out = capsys.readouterr().out
76
92
  assert "itemId" in out
77
93
 
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.1.0"
2
+ __version__ = "0.1.1"
@@ -1,14 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import re
4
5
  from dataclasses import dataclass
5
6
  from typing import Any
7
+ from urllib.parse import urlencode
6
8
 
7
9
  import requests
8
10
  from requests import RequestException
9
11
 
10
12
 
11
13
  BASE_URL = "https://www.tradera.com"
14
+ ALLOWED_BUYER_REGIONS = {
15
+ "sweden": "sweden;eu;international",
16
+ "eu": "eu;international",
17
+ "international": "international",
18
+ }
12
19
 
13
20
 
14
21
  class TraderaApiError(RuntimeError):
@@ -25,7 +32,7 @@ class TraderaClient:
25
32
  self.session.headers.update(
26
33
  {
27
34
  "accept": "application/json, text/plain, */*",
28
- "user-agent": "tradera-cli/0.1.0",
35
+ "user-agent": "tradera-cli/0.1.1",
29
36
  }
30
37
  )
31
38
 
@@ -84,7 +91,43 @@ class TraderaClient:
84
91
  language_code_iso2: str = "sv",
85
92
  shipping_country_code_iso2: str = "SE",
86
93
  automatic_translation_preferred: bool = True,
94
+ item_status: str | None = None,
95
+ condition: str | None = None,
96
+ item_type: str | None = None,
97
+ from_price: int | None = None,
98
+ to_price: int | None = None,
99
+ allowed_buyer_regions: str | None = None,
100
+ counties: list[str] | None = None,
101
+ search_type: str | None = None,
87
102
  ) -> dict[str, Any]:
103
+ normalized_item_status = item_status or "Active"
104
+ page_filters: dict[str, str] = {}
105
+ if search_type:
106
+ page_filters["searchType"] = search_type
107
+ if normalized_item_status in {"Sold", "Unsold"}:
108
+ page_filters["itemStatus"] = normalized_item_status
109
+ if condition:
110
+ page_filters["af-condition"] = condition
111
+ if item_type and item_type != "All":
112
+ page_filters["itemType"] = item_type
113
+ if from_price is not None:
114
+ page_filters["fromPrice"] = str(from_price)
115
+ if to_price is not None:
116
+ page_filters["toPrice"] = str(to_price)
117
+ if allowed_buyer_regions:
118
+ page_filters["allowedBuyerRegions"] = ALLOWED_BUYER_REGIONS[allowed_buyer_regions]
119
+ if counties:
120
+ page_filters["counties"] = ";".join(counties)
121
+
122
+ if page_filters:
123
+ return self._search_page(
124
+ query=query,
125
+ page=page,
126
+ sort_by=sort_by,
127
+ language_code_iso2=language_code_iso2,
128
+ filters=page_filters,
129
+ )
130
+
88
131
  payload: dict[str, Any] = {
89
132
  "isCsaSearchQuery": False,
90
133
  "query": query,
@@ -118,6 +161,39 @@ class TraderaClient:
118
161
  }
119
162
  return self._request("POST", "/api/webapi/discover/web/independent-search", json=payload)
120
163
 
164
+ def _search_page(
165
+ self,
166
+ query: str,
167
+ page: int,
168
+ sort_by: str,
169
+ language_code_iso2: str,
170
+ filters: dict[str, str],
171
+ ) -> dict[str, Any]:
172
+ params = {
173
+ "q": query,
174
+ "paging": page,
175
+ "sortBy": sort_by,
176
+ "languageCodeIso2": language_code_iso2.lower(),
177
+ }
178
+ params.update(filters)
179
+ html = self._request("GET", f"/search?{urlencode(params)}")
180
+ if not isinstance(html, str):
181
+ raise TraderaApiError("Unexpected search page response")
182
+
183
+ match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html)
184
+ if not match:
185
+ raise TraderaApiError("Could not parse search page response")
186
+
187
+ try:
188
+ data = json.loads(match.group(1))
189
+ except ValueError as exc:
190
+ raise TraderaApiError("Invalid search page data") from exc
191
+
192
+ discover = data.get("props", {}).get("pageProps", {}).get("initialState", {}).get("discover")
193
+ if not isinstance(discover, dict):
194
+ raise TraderaApiError("Unexpected search page data")
195
+ return discover
196
+
121
197
  def item(self, item_id: int) -> dict[str, Any]:
122
198
  data = self._request("GET", f"/ajax/item/{item_id}")
123
199
  if not isinstance(data, dict):
@@ -34,6 +34,14 @@ def cmd_search(args: argparse.Namespace) -> int:
34
34
  language_code_iso2=args.lang,
35
35
  shipping_country_code_iso2=args.country,
36
36
  automatic_translation_preferred=not args.no_translate,
37
+ item_status=args.item_status,
38
+ condition=args.condition,
39
+ item_type=args.item_type,
40
+ from_price=args.from_price,
41
+ to_price=args.to_price,
42
+ allowed_buyer_regions=args.allowed_buyer_regions,
43
+ counties=args.counties,
44
+ search_type=args.search_type,
37
45
  )
38
46
  rows = normalize_search_rows(data)
39
47
  _print_output(
@@ -82,6 +90,14 @@ def build_parser() -> argparse.ArgumentParser:
82
90
  search.add_argument("--sort", default="Relevance")
83
91
  search.add_argument("--lang", default="sv")
84
92
  search.add_argument("--country", default="SE")
93
+ search.add_argument("--item-status", choices=["Active", "Sold", "Unsold"])
94
+ search.add_argument("--condition", choices=["Oanvänt", "Mycket gott skick", "Gott skick", "Okej skick", "Defekt"])
95
+ search.add_argument("--item-type", choices=["All", "Auction", "FixedPrice", "ContactOnly"])
96
+ search.add_argument("--from-price", type=int)
97
+ search.add_argument("--to-price", type=int)
98
+ search.add_argument("--allowed-buyer-regions", choices=["sweden", "eu", "international"])
99
+ search.add_argument("--counties", nargs="+")
100
+ search.add_argument("--search-type", choices=["ExactSearch"])
85
101
  search.add_argument("--no-translate", action="store_true")
86
102
  search.add_argument("--format", choices=["table", "json", "jsonl"], default="table")
87
103
  search.set_defaults(func=cmd_search)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradera-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: CLI for Tradera web endpoints
5
5
  Author: Johnny W
6
6
  License-Expression: MIT
@@ -69,6 +69,11 @@ tradera search "iphone"
69
69
  tradera search "pokemon" --page 2 --page-size 20
70
70
  tradera search "kamera" --sort AddedOn --format json
71
71
  tradera search "klocka" --country SE --lang sv --format jsonl
72
+ tradera search "lego" --item-status Sold --format json
73
+ tradera search "lego" --condition "Oanvänt" --format json
74
+ tradera search "lego" --item-type FixedPrice --from-price 100 --to-price 500
75
+ tradera search "lego" --allowed-buyer-regions eu --counties Stockholm Uppsala
76
+ tradera search "lego city" --search-type ExactSearch
72
77
  ```
73
78
 
74
79
  ### Get item details
@@ -105,6 +110,14 @@ Search command (`tradera search`) supports:
105
110
  | `--sort` | Sort mode (default: `Relevance`) |
106
111
  | `--lang` | Language code (default: `sv`) |
107
112
  | `--country` | Shipping country code (default: `SE`) |
113
+ | `--item-status` | Filter by item status: `Active`, `Sold`, `Unsold` |
114
+ | `--condition` | Filter by condition: `Oanvänt`, `Mycket gott skick`, `Gott skick`, `Okej skick`, `Defekt` |
115
+ | `--item-type` | Filter by listing type: `All`, `Auction`, `FixedPrice`, `ContactOnly` |
116
+ | `--from-price` | Minimum price |
117
+ | `--to-price` | Maximum price |
118
+ | `--allowed-buyer-regions` | Buyer region filter: `sweden`, `eu`, `international` |
119
+ | `--counties` | Pickup counties, passed as one or more values, for example `Stockholm Uppsala` |
120
+ | `--search-type` | Search mode. Current supported value: `ExactSearch` |
108
121
  | `--no-translate` | Disable automatic translation preference |
109
122
  | `--format` | Output format: `table`, `json`, `jsonl` |
110
123
 
File without changes
File without changes