trawl-api 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.
- trawl_api-0.1.0/.gitignore +10 -0
- trawl_api-0.1.0/CHANGELOG.md +8 -0
- trawl_api-0.1.0/LICENSE +21 -0
- trawl_api-0.1.0/PKG-INFO +273 -0
- trawl_api-0.1.0/README.md +242 -0
- trawl_api-0.1.0/examples/average_price.py +31 -0
- trawl_api-0.1.0/examples/errors_and_credits.py +33 -0
- trawl_api-0.1.0/examples/export_csv.py +42 -0
- trawl_api-0.1.0/examples/find_category.py +28 -0
- trawl_api-0.1.0/examples/item_details.py +38 -0
- trawl_api-0.1.0/examples/sold_prices.py +20 -0
- trawl_api-0.1.0/pyproject.toml +80 -0
- trawl_api-0.1.0/src/trawl_api/__init__.py +63 -0
- trawl_api-0.1.0/src/trawl_api/_client.py +251 -0
- trawl_api-0.1.0/src/trawl_api/_errors.py +60 -0
- trawl_api-0.1.0/src/trawl_api/_models.py +189 -0
- trawl_api-0.1.0/src/trawl_api/_version.py +1 -0
- trawl_api-0.1.0/src/trawl_api/ebay.py +203 -0
- trawl_api-0.1.0/src/trawl_api/py.typed +0 -0
- trawl_api-0.1.0/tests/conftest.py +5 -0
- trawl_api-0.1.0/tests/samples.py +112 -0
- trawl_api-0.1.0/tests/test_client.py +292 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project follows [Semantic Versioning](https://semver.org).
|
|
4
|
+
|
|
5
|
+
## 0.1.0 (2026-10-06)
|
|
6
|
+
|
|
7
|
+
- First release: `Trawl` and `AsyncTrawl` clients with `ebay.sold`, `ebay.item` and
|
|
8
|
+
`ebay.categories`, typed responses, typed errors and automatic retries.
|
trawl_api-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 trawl
|
|
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.
|
trawl_api-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: trawl-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python eBay scraper API: sold listings, prices and item details from eBay as JSON
|
|
5
|
+
Project-URL: Homepage, https://trawl.dev
|
|
6
|
+
Project-URL: Documentation, https://trawl.dev/docs
|
|
7
|
+
Project-URL: Pricing, https://trawl.dev/pricing
|
|
8
|
+
Project-URL: Repository, https://github.com/trawl-inc/trawl-python
|
|
9
|
+
Project-URL: Changelog, https://github.com/trawl-inc/trawl-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author: trawl
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ebay,ebay api,ebay price data,ebay scraper,ebay sold listings,scraper,sold prices,trawl
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: httpx<1,>=0.25
|
|
29
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# trawl-api: Python eBay scraper API for sold listings and prices
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/trawl-api/)
|
|
35
|
+
[](https://pypi.org/project/trawl-api/)
|
|
36
|
+
|
|
37
|
+
The official Python client for [trawl](https://trawl.dev), a data API for eBay. Search 300+
|
|
38
|
+
million completed eBay sales on the US and UK marketplaces and get each one back as typed
|
|
39
|
+
Python objects: final price, sale date, condition, shipping, seller, item specifics, images
|
|
40
|
+
and description. One API key, no proxies, no HTML parsing, no browser to run.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install trawl-api
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python 3.10 or newer. [Create a free account](https://trawl.dev/signup) to get an
|
|
47
|
+
API key; the free plan needs no card.
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from trawl_api import Trawl
|
|
53
|
+
|
|
54
|
+
client = Trawl() # reads the TRAWL_API_KEY environment variable
|
|
55
|
+
|
|
56
|
+
sold = client.ebay.sold("iphone 15 pro 256gb", condition="used")
|
|
57
|
+
|
|
58
|
+
for listing in sold.results[:5]:
|
|
59
|
+
print(listing.date_sold.date(), listing.sale_price, listing.title)
|
|
60
|
+
|
|
61
|
+
print(f"{sold.count} results, {sold.credits_charged} credit(s) charged")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The key can also be passed directly: `Trawl(api_key="sk_live_...")`. Keep it on your server
|
|
65
|
+
and out of source control.
|
|
66
|
+
|
|
67
|
+
## What you can ask
|
|
68
|
+
|
|
69
|
+
| Method | Answers | Costs |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `client.ebay.sold(query, ...)` | Sold listings matching a query, newest first, up to 2,000 in one response | 1 credit per page of 100 results (2 with `details=True`); free when nothing matches |
|
|
72
|
+
| `client.ebay.item(item_id)` | One sold listing in full: specifics, seller, every image, description, recorded sales | 1 credit |
|
|
73
|
+
| `client.ebay.categories(query)` | eBay categories by name or id, for the `category` filter | 1 credit; free when nothing matches |
|
|
74
|
+
|
|
75
|
+
The full parameter and field reference is at [trawl.dev/docs](https://trawl.dev/docs).
|
|
76
|
+
|
|
77
|
+
## Tutorials
|
|
78
|
+
|
|
79
|
+
Each of these is a complete script in
|
|
80
|
+
[`examples/`](https://github.com/trawl-inc/trawl-python/tree/main/examples).
|
|
81
|
+
|
|
82
|
+
### Get the sold prices of a product
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from trawl_api import Trawl
|
|
86
|
+
|
|
87
|
+
client = Trawl()
|
|
88
|
+
sold = client.ebay.sold(
|
|
89
|
+
"iphone 15 pro 256gb",
|
|
90
|
+
condition="used",
|
|
91
|
+
exclude=["case", "cracked"], # words that must not be in the title
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
for listing in sold.results:
|
|
95
|
+
print(
|
|
96
|
+
f"{listing.date_sold:%Y-%m-%d} {listing.currency}{listing.sale_price:.2f} {listing.title}"
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Average sold price over the last 90 days
|
|
101
|
+
|
|
102
|
+
`max_pages` sets how many pages of 100 results come back in the one response, and is also the
|
|
103
|
+
most credits the call can cost.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from datetime import date, timedelta
|
|
107
|
+
from statistics import mean, median
|
|
108
|
+
|
|
109
|
+
from trawl_api import Trawl
|
|
110
|
+
|
|
111
|
+
client = Trawl()
|
|
112
|
+
sold = client.ebay.sold(
|
|
113
|
+
"nintendo switch oled",
|
|
114
|
+
condition="used",
|
|
115
|
+
date_from=date.today() - timedelta(days=90),
|
|
116
|
+
max_pages=5,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
prices = [listing.sale_price for listing in sold.results]
|
|
120
|
+
print(
|
|
121
|
+
f"{len(prices)} sales, average {mean(prices):.2f}, median {median(prices):.2f} {sold.currency}"
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Filter by price, marketplace and item specifics
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
sold = client.ebay.sold(
|
|
129
|
+
"charizard",
|
|
130
|
+
site="EBAY_GB", # ebay.co.uk; prices are then in GBP
|
|
131
|
+
min_price=100,
|
|
132
|
+
max_price=2000,
|
|
133
|
+
attr={"Set": "Base Set", "Grade": ["9", "10"]}, # Grade 9 or 10, from Base Set
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Export sold listings to CSV or pandas
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import csv
|
|
141
|
+
|
|
142
|
+
from trawl_api import Trawl
|
|
143
|
+
|
|
144
|
+
COLUMNS = ["date_sold", "title", "sale_price", "currency", "condition", "item_id", "item_link"]
|
|
145
|
+
|
|
146
|
+
client = Trawl()
|
|
147
|
+
sold = client.ebay.sold("charizard base set holo", max_pages=10)
|
|
148
|
+
|
|
149
|
+
with open("sold.csv", "w", newline="", encoding="utf-8") as file:
|
|
150
|
+
writer = csv.DictWriter(file, fieldnames=COLUMNS)
|
|
151
|
+
writer.writeheader()
|
|
152
|
+
for listing in sold.results:
|
|
153
|
+
writer.writerow(listing.model_dump(mode="json", include=set(COLUMNS)))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For pandas: `pd.DataFrame(listing.model_dump() for listing in sold.results)`.
|
|
157
|
+
|
|
158
|
+
### Get one listing's full details
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from trawl_api import NotFoundError, Trawl
|
|
162
|
+
|
|
163
|
+
client = Trawl()
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
item = client.ebay.item("256637082114")
|
|
167
|
+
except NotFoundError:
|
|
168
|
+
# Details arrive a few minutes after a sale. A 404 is never billed.
|
|
169
|
+
raise SystemExit("Details are not available yet.")
|
|
170
|
+
|
|
171
|
+
print(item.title, item.sale_price, item.currency)
|
|
172
|
+
print(item.specifics["Brand"]) # item specifics as a dict
|
|
173
|
+
print(item.seller.username, item.seller.feedback_percent)
|
|
174
|
+
print(len(item.images), "images")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
To get the details of every result of a search in one call, pass `details=True` to `sold()`
|
|
178
|
+
and read `listing.details`.
|
|
179
|
+
|
|
180
|
+
### Find a category and search inside it
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
found = client.ebay.categories("trading card singles", site="EBAY_US")
|
|
184
|
+
for category in found.categories:
|
|
185
|
+
print(category.category_id, category.name, category.group)
|
|
186
|
+
|
|
187
|
+
sold = client.ebay.sold("charizard", category=found.categories[0].category_id)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Category ids differ per marketplace, so look them up with the same `site` you search with.
|
|
191
|
+
|
|
192
|
+
## Credits
|
|
193
|
+
|
|
194
|
+
Every answer says what it cost, and how much of the month's allowance is left:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
sold = client.ebay.sold("rolex submariner", max_pages=3)
|
|
198
|
+
|
|
199
|
+
sold.credits_charged # 3 when three pages came back, 0 when nothing matched
|
|
200
|
+
sold.rate_limit.remaining # credits left in this billing window
|
|
201
|
+
sold.rate_limit.reset # when the allowance resets
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Errors are never billed.
|
|
205
|
+
|
|
206
|
+
## Errors
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
import trawl_api
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
sold = client.ebay.sold("rolex submariner", min_price=2000)
|
|
213
|
+
except trawl_api.BadRequestError as error:
|
|
214
|
+
print(error) # the API names the field and the rule it broke
|
|
215
|
+
except trawl_api.InsufficientCreditsError as error:
|
|
216
|
+
print(error) # the month's credits are spent, or too few remain
|
|
217
|
+
except trawl_api.RateLimitError as error:
|
|
218
|
+
print(error.retry_after) # seconds to wait
|
|
219
|
+
except trawl_api.APIStatusError as error:
|
|
220
|
+
print(error.status_code, error)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Exception | When |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `BadRequestError` | 400, a parameter failed validation |
|
|
226
|
+
| `AuthenticationError` | 403, the key is missing, invalid or deleted |
|
|
227
|
+
| `NotFoundError` | 404, e.g. an item whose details are not available yet |
|
|
228
|
+
| `RateLimitError` | 429 with `Retry-After`, the plan's per-second rate was exceeded |
|
|
229
|
+
| `InsufficientCreditsError` | 429 without `Retry-After`, not enough credits for the request |
|
|
230
|
+
| `ServerError` | 5xx, a failure on trawl's side |
|
|
231
|
+
| `APIConnectionError`, `APITimeoutError` | the request got no answer |
|
|
232
|
+
|
|
233
|
+
All of them inherit from `trawl_api.TrawlError`. Connection errors, 5xx answers and
|
|
234
|
+
per-second rate limits are retried twice with backoff before they are raised; change that
|
|
235
|
+
with `Trawl(max_retries=...)`.
|
|
236
|
+
|
|
237
|
+
## Async
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import asyncio
|
|
241
|
+
|
|
242
|
+
from trawl_api import AsyncTrawl
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def main():
|
|
246
|
+
async with AsyncTrawl() as client:
|
|
247
|
+
sold = await client.ebay.sold("iphone 15 pro 256gb")
|
|
248
|
+
print(sold.count)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
asyncio.run(main())
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Configuration
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
client = Trawl(
|
|
258
|
+
api_key="sk_live_...", # default: the TRAWL_API_KEY environment variable
|
|
259
|
+
timeout=60.0, # seconds
|
|
260
|
+
max_retries=2,
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Responses are [pydantic](https://docs.pydantic.dev) models: `model_dump()` gives a dict,
|
|
265
|
+
`model_dump_json()` a JSON string, and fields the API adds later are kept on the object.
|
|
266
|
+
|
|
267
|
+
## Links
|
|
268
|
+
|
|
269
|
+
- [Documentation](https://trawl.dev/docs)
|
|
270
|
+
- [Pricing](https://trawl.dev/pricing)
|
|
271
|
+
- [Changelog](https://github.com/trawl-inc/trawl-python/blob/main/CHANGELOG.md)
|
|
272
|
+
|
|
273
|
+
trawl is an independent service and is not affiliated with or endorsed by eBay Inc.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# trawl-api: Python eBay scraper API for sold listings and prices
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/trawl-api/)
|
|
4
|
+
[](https://pypi.org/project/trawl-api/)
|
|
5
|
+
|
|
6
|
+
The official Python client for [trawl](https://trawl.dev), a data API for eBay. Search 300+
|
|
7
|
+
million completed eBay sales on the US and UK marketplaces and get each one back as typed
|
|
8
|
+
Python objects: final price, sale date, condition, shipping, seller, item specifics, images
|
|
9
|
+
and description. One API key, no proxies, no HTML parsing, no browser to run.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install trawl-api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.10 or newer. [Create a free account](https://trawl.dev/signup) to get an
|
|
16
|
+
API key; the free plan needs no card.
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from trawl_api import Trawl
|
|
22
|
+
|
|
23
|
+
client = Trawl() # reads the TRAWL_API_KEY environment variable
|
|
24
|
+
|
|
25
|
+
sold = client.ebay.sold("iphone 15 pro 256gb", condition="used")
|
|
26
|
+
|
|
27
|
+
for listing in sold.results[:5]:
|
|
28
|
+
print(listing.date_sold.date(), listing.sale_price, listing.title)
|
|
29
|
+
|
|
30
|
+
print(f"{sold.count} results, {sold.credits_charged} credit(s) charged")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The key can also be passed directly: `Trawl(api_key="sk_live_...")`. Keep it on your server
|
|
34
|
+
and out of source control.
|
|
35
|
+
|
|
36
|
+
## What you can ask
|
|
37
|
+
|
|
38
|
+
| Method | Answers | Costs |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `client.ebay.sold(query, ...)` | Sold listings matching a query, newest first, up to 2,000 in one response | 1 credit per page of 100 results (2 with `details=True`); free when nothing matches |
|
|
41
|
+
| `client.ebay.item(item_id)` | One sold listing in full: specifics, seller, every image, description, recorded sales | 1 credit |
|
|
42
|
+
| `client.ebay.categories(query)` | eBay categories by name or id, for the `category` filter | 1 credit; free when nothing matches |
|
|
43
|
+
|
|
44
|
+
The full parameter and field reference is at [trawl.dev/docs](https://trawl.dev/docs).
|
|
45
|
+
|
|
46
|
+
## Tutorials
|
|
47
|
+
|
|
48
|
+
Each of these is a complete script in
|
|
49
|
+
[`examples/`](https://github.com/trawl-inc/trawl-python/tree/main/examples).
|
|
50
|
+
|
|
51
|
+
### Get the sold prices of a product
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from trawl_api import Trawl
|
|
55
|
+
|
|
56
|
+
client = Trawl()
|
|
57
|
+
sold = client.ebay.sold(
|
|
58
|
+
"iphone 15 pro 256gb",
|
|
59
|
+
condition="used",
|
|
60
|
+
exclude=["case", "cracked"], # words that must not be in the title
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for listing in sold.results:
|
|
64
|
+
print(
|
|
65
|
+
f"{listing.date_sold:%Y-%m-%d} {listing.currency}{listing.sale_price:.2f} {listing.title}"
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Average sold price over the last 90 days
|
|
70
|
+
|
|
71
|
+
`max_pages` sets how many pages of 100 results come back in the one response, and is also the
|
|
72
|
+
most credits the call can cost.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from datetime import date, timedelta
|
|
76
|
+
from statistics import mean, median
|
|
77
|
+
|
|
78
|
+
from trawl_api import Trawl
|
|
79
|
+
|
|
80
|
+
client = Trawl()
|
|
81
|
+
sold = client.ebay.sold(
|
|
82
|
+
"nintendo switch oled",
|
|
83
|
+
condition="used",
|
|
84
|
+
date_from=date.today() - timedelta(days=90),
|
|
85
|
+
max_pages=5,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
prices = [listing.sale_price for listing in sold.results]
|
|
89
|
+
print(
|
|
90
|
+
f"{len(prices)} sales, average {mean(prices):.2f}, median {median(prices):.2f} {sold.currency}"
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Filter by price, marketplace and item specifics
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
sold = client.ebay.sold(
|
|
98
|
+
"charizard",
|
|
99
|
+
site="EBAY_GB", # ebay.co.uk; prices are then in GBP
|
|
100
|
+
min_price=100,
|
|
101
|
+
max_price=2000,
|
|
102
|
+
attr={"Set": "Base Set", "Grade": ["9", "10"]}, # Grade 9 or 10, from Base Set
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Export sold listings to CSV or pandas
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import csv
|
|
110
|
+
|
|
111
|
+
from trawl_api import Trawl
|
|
112
|
+
|
|
113
|
+
COLUMNS = ["date_sold", "title", "sale_price", "currency", "condition", "item_id", "item_link"]
|
|
114
|
+
|
|
115
|
+
client = Trawl()
|
|
116
|
+
sold = client.ebay.sold("charizard base set holo", max_pages=10)
|
|
117
|
+
|
|
118
|
+
with open("sold.csv", "w", newline="", encoding="utf-8") as file:
|
|
119
|
+
writer = csv.DictWriter(file, fieldnames=COLUMNS)
|
|
120
|
+
writer.writeheader()
|
|
121
|
+
for listing in sold.results:
|
|
122
|
+
writer.writerow(listing.model_dump(mode="json", include=set(COLUMNS)))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For pandas: `pd.DataFrame(listing.model_dump() for listing in sold.results)`.
|
|
126
|
+
|
|
127
|
+
### Get one listing's full details
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from trawl_api import NotFoundError, Trawl
|
|
131
|
+
|
|
132
|
+
client = Trawl()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
item = client.ebay.item("256637082114")
|
|
136
|
+
except NotFoundError:
|
|
137
|
+
# Details arrive a few minutes after a sale. A 404 is never billed.
|
|
138
|
+
raise SystemExit("Details are not available yet.")
|
|
139
|
+
|
|
140
|
+
print(item.title, item.sale_price, item.currency)
|
|
141
|
+
print(item.specifics["Brand"]) # item specifics as a dict
|
|
142
|
+
print(item.seller.username, item.seller.feedback_percent)
|
|
143
|
+
print(len(item.images), "images")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To get the details of every result of a search in one call, pass `details=True` to `sold()`
|
|
147
|
+
and read `listing.details`.
|
|
148
|
+
|
|
149
|
+
### Find a category and search inside it
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
found = client.ebay.categories("trading card singles", site="EBAY_US")
|
|
153
|
+
for category in found.categories:
|
|
154
|
+
print(category.category_id, category.name, category.group)
|
|
155
|
+
|
|
156
|
+
sold = client.ebay.sold("charizard", category=found.categories[0].category_id)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Category ids differ per marketplace, so look them up with the same `site` you search with.
|
|
160
|
+
|
|
161
|
+
## Credits
|
|
162
|
+
|
|
163
|
+
Every answer says what it cost, and how much of the month's allowance is left:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
sold = client.ebay.sold("rolex submariner", max_pages=3)
|
|
167
|
+
|
|
168
|
+
sold.credits_charged # 3 when three pages came back, 0 when nothing matched
|
|
169
|
+
sold.rate_limit.remaining # credits left in this billing window
|
|
170
|
+
sold.rate_limit.reset # when the allowance resets
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Errors are never billed.
|
|
174
|
+
|
|
175
|
+
## Errors
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import trawl_api
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
sold = client.ebay.sold("rolex submariner", min_price=2000)
|
|
182
|
+
except trawl_api.BadRequestError as error:
|
|
183
|
+
print(error) # the API names the field and the rule it broke
|
|
184
|
+
except trawl_api.InsufficientCreditsError as error:
|
|
185
|
+
print(error) # the month's credits are spent, or too few remain
|
|
186
|
+
except trawl_api.RateLimitError as error:
|
|
187
|
+
print(error.retry_after) # seconds to wait
|
|
188
|
+
except trawl_api.APIStatusError as error:
|
|
189
|
+
print(error.status_code, error)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Exception | When |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `BadRequestError` | 400, a parameter failed validation |
|
|
195
|
+
| `AuthenticationError` | 403, the key is missing, invalid or deleted |
|
|
196
|
+
| `NotFoundError` | 404, e.g. an item whose details are not available yet |
|
|
197
|
+
| `RateLimitError` | 429 with `Retry-After`, the plan's per-second rate was exceeded |
|
|
198
|
+
| `InsufficientCreditsError` | 429 without `Retry-After`, not enough credits for the request |
|
|
199
|
+
| `ServerError` | 5xx, a failure on trawl's side |
|
|
200
|
+
| `APIConnectionError`, `APITimeoutError` | the request got no answer |
|
|
201
|
+
|
|
202
|
+
All of them inherit from `trawl_api.TrawlError`. Connection errors, 5xx answers and
|
|
203
|
+
per-second rate limits are retried twice with backoff before they are raised; change that
|
|
204
|
+
with `Trawl(max_retries=...)`.
|
|
205
|
+
|
|
206
|
+
## Async
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
import asyncio
|
|
210
|
+
|
|
211
|
+
from trawl_api import AsyncTrawl
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def main():
|
|
215
|
+
async with AsyncTrawl() as client:
|
|
216
|
+
sold = await client.ebay.sold("iphone 15 pro 256gb")
|
|
217
|
+
print(sold.count)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
asyncio.run(main())
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Configuration
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
client = Trawl(
|
|
227
|
+
api_key="sk_live_...", # default: the TRAWL_API_KEY environment variable
|
|
228
|
+
timeout=60.0, # seconds
|
|
229
|
+
max_retries=2,
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Responses are [pydantic](https://docs.pydantic.dev) models: `model_dump()` gives a dict,
|
|
234
|
+
`model_dump_json()` a JSON string, and fields the API adds later are kept on the object.
|
|
235
|
+
|
|
236
|
+
## Links
|
|
237
|
+
|
|
238
|
+
- [Documentation](https://trawl.dev/docs)
|
|
239
|
+
- [Pricing](https://trawl.dev/pricing)
|
|
240
|
+
- [Changelog](https://github.com/trawl-inc/trawl-python/blob/main/CHANGELOG.md)
|
|
241
|
+
|
|
242
|
+
trawl is an independent service and is not affiliated with or endorsed by eBay Inc.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Average and median sold price over the last 90 days.
|
|
2
|
+
|
|
3
|
+
export TRAWL_API_KEY=sk_live_...
|
|
4
|
+
python examples/average_price.py "nintendo switch oled"
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import date, timedelta
|
|
9
|
+
from statistics import mean, median
|
|
10
|
+
|
|
11
|
+
from trawl_api import Trawl
|
|
12
|
+
|
|
13
|
+
query = sys.argv[1] if len(sys.argv) > 1 else "nintendo switch oled"
|
|
14
|
+
|
|
15
|
+
client = Trawl()
|
|
16
|
+
sold = client.ebay.sold(
|
|
17
|
+
query,
|
|
18
|
+
condition="used",
|
|
19
|
+
date_from=date.today() - timedelta(days=90),
|
|
20
|
+
max_pages=5, # up to 500 sales, and at most 5 credits
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
prices = [listing.sale_price for listing in sold.results]
|
|
24
|
+
if not prices:
|
|
25
|
+
sys.exit(f"No sales of {query!r} in the last 90 days.")
|
|
26
|
+
|
|
27
|
+
print(f"{query}: {len(prices)} sales in the last 90 days ({sold.currency})")
|
|
28
|
+
print(f" average {mean(prices):.2f}")
|
|
29
|
+
print(f" median {median(prices):.2f}")
|
|
30
|
+
print(f" range {min(prices):.2f} to {max(prices):.2f}")
|
|
31
|
+
print(f"{sold.credits_charged} credit(s) charged")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Handle the API's errors and keep track of credits.
|
|
2
|
+
|
|
3
|
+
export TRAWL_API_KEY=sk_live_...
|
|
4
|
+
python examples/errors_and_credits.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import trawl_api
|
|
8
|
+
from trawl_api import Trawl
|
|
9
|
+
|
|
10
|
+
client = Trawl(max_retries=3, timeout=30)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
sold = client.ebay.sold("rolex submariner", min_price=2000, max_pages=3)
|
|
14
|
+
except trawl_api.BadRequestError as error:
|
|
15
|
+
# A parameter failed validation; the message names the field and the rule.
|
|
16
|
+
print(f"Bad request: {error}")
|
|
17
|
+
except trawl_api.AuthenticationError:
|
|
18
|
+
print("The API key is missing, invalid or deleted.")
|
|
19
|
+
except trawl_api.InsufficientCreditsError as error:
|
|
20
|
+
# The month's credits are spent, or too few remain for this max_pages.
|
|
21
|
+
print(f"Not enough credits: {error}")
|
|
22
|
+
except trawl_api.RateLimitError as error:
|
|
23
|
+
# Raised only after the client's own retries; wait this long and try again.
|
|
24
|
+
print(f"Rate limited, retry in {error.retry_after}s")
|
|
25
|
+
except trawl_api.APIConnectionError:
|
|
26
|
+
print("Could not reach the API.")
|
|
27
|
+
except trawl_api.APIStatusError as error:
|
|
28
|
+
print(f"The API answered {error.status_code}: {error}")
|
|
29
|
+
else:
|
|
30
|
+
print(f"{sold.count} results for {sold.credits_charged} credit(s)")
|
|
31
|
+
if sold.rate_limit:
|
|
32
|
+
print(f"{sold.rate_limit.remaining} of {sold.rate_limit.limit} credits left")
|
|
33
|
+
print(f"The allowance resets on {sold.rate_limit.reset:%Y-%m-%d}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Export sold listings to a CSV file.
|
|
2
|
+
|
|
3
|
+
export TRAWL_API_KEY=sk_live_...
|
|
4
|
+
python examples/export_csv.py "charizard base set holo" sold.csv
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from trawl_api import Trawl
|
|
11
|
+
|
|
12
|
+
query = sys.argv[1] if len(sys.argv) > 1 else "charizard base set holo"
|
|
13
|
+
path = sys.argv[2] if len(sys.argv) > 2 else "sold.csv"
|
|
14
|
+
|
|
15
|
+
COLUMNS = [
|
|
16
|
+
"date_sold",
|
|
17
|
+
"title",
|
|
18
|
+
"sale_price",
|
|
19
|
+
"shipping_price",
|
|
20
|
+
"currency",
|
|
21
|
+
"condition",
|
|
22
|
+
"buying_format",
|
|
23
|
+
"bids",
|
|
24
|
+
"location",
|
|
25
|
+
"item_id",
|
|
26
|
+
"item_link",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
client = Trawl()
|
|
30
|
+
sold = client.ebay.sold(query, max_pages=10) # up to 1,000 sales, and at most 10 credits
|
|
31
|
+
|
|
32
|
+
with open(path, "w", newline="", encoding="utf-8") as file:
|
|
33
|
+
writer = csv.DictWriter(file, fieldnames=COLUMNS)
|
|
34
|
+
writer.writeheader()
|
|
35
|
+
for listing in sold.results:
|
|
36
|
+
writer.writerow(listing.model_dump(mode="json", include=set(COLUMNS)))
|
|
37
|
+
|
|
38
|
+
print(f"Wrote {sold.count} sales to {path} ({sold.credits_charged} credit(s) charged)")
|
|
39
|
+
|
|
40
|
+
# With pandas instead:
|
|
41
|
+
# import pandas as pd
|
|
42
|
+
# frame = pd.DataFrame(listing.model_dump() for listing in sold.results)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Find an eBay category id by name, then search inside it.
|
|
2
|
+
|
|
3
|
+
export TRAWL_API_KEY=sk_live_...
|
|
4
|
+
python examples/find_category.py "trading card singles" "charizard"
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from trawl_api import Trawl
|
|
10
|
+
|
|
11
|
+
category_query = sys.argv[1] if len(sys.argv) > 1 else "trading card singles"
|
|
12
|
+
query = sys.argv[2] if len(sys.argv) > 2 else "charizard"
|
|
13
|
+
|
|
14
|
+
client = Trawl()
|
|
15
|
+
|
|
16
|
+
# Category ids differ per marketplace: look them up on the one you will search.
|
|
17
|
+
found = client.ebay.categories(category_query, site="EBAY_US")
|
|
18
|
+
if not found.categories:
|
|
19
|
+
sys.exit(f"No category matches {category_query!r}.")
|
|
20
|
+
|
|
21
|
+
for category in found.categories:
|
|
22
|
+
print(f"{category.category_id:>8} {category.name} ({category.group})")
|
|
23
|
+
|
|
24
|
+
best = found.categories[0]
|
|
25
|
+
sold = client.ebay.sold(query, category=best.category_id, site="EBAY_US")
|
|
26
|
+
print(f"\n{sold.count} sales of {query!r} in {best.name}:")
|
|
27
|
+
for listing in sold.results[:10]:
|
|
28
|
+
print(f" {listing.currency}{listing.sale_price:>9.2f} {listing.title}")
|