hepsijet 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.
- hepsijet-0.1.0/.gitignore +7 -0
- hepsijet-0.1.0/LICENSE +21 -0
- hepsijet-0.1.0/PKG-INFO +335 -0
- hepsijet-0.1.0/README.md +307 -0
- hepsijet-0.1.0/examples/django_example.py +98 -0
- hepsijet-0.1.0/examples/fastapi_example.py +89 -0
- hepsijet-0.1.0/pyproject.toml +60 -0
- hepsijet-0.1.0/src/hepsijet/__init__.py +83 -0
- hepsijet-0.1.0/src/hepsijet/_auth.py +70 -0
- hepsijet-0.1.0/src/hepsijet/_endpoints.py +285 -0
- hepsijet-0.1.0/src/hepsijet/_payloads.py +265 -0
- hepsijet-0.1.0/src/hepsijet/async_client.py +188 -0
- hepsijet-0.1.0/src/hepsijet/client.py +182 -0
- hepsijet-0.1.0/src/hepsijet/config.py +32 -0
- hepsijet-0.1.0/src/hepsijet/enums.py +67 -0
- hepsijet-0.1.0/src/hepsijet/errors.py +36 -0
- hepsijet-0.1.0/src/hepsijet/models/__init__.py +42 -0
- hepsijet-0.1.0/src/hepsijet/models/base.py +37 -0
- hepsijet-0.1.0/src/hepsijet/models/merchants.py +22 -0
- hepsijet-0.1.0/src/hepsijet/models/people.py +42 -0
- hepsijet-0.1.0/src/hepsijet/models/responses.py +111 -0
- hepsijet-0.1.0/src/hepsijet/models/shipments.py +78 -0
- hepsijet-0.1.0/src/hepsijet/models/updates.py +73 -0
- hepsijet-0.1.0/src/hepsijet/py.typed +0 -0
- hepsijet-0.1.0/tests/conftest.py +96 -0
- hepsijet-0.1.0/tests/test_client.py +264 -0
- hepsijet-0.1.0/uv.lock +725 -0
hepsijet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omer
|
|
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.
|
hepsijet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hepsijet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A typed, readable Python SDK for the HepsiJET cargo API (sync + async).
|
|
5
|
+
Project-URL: Homepage, https://github.com/otomatikportal/hepsijet-unofficial-python-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/otomatikportal/hepsijet-unofficial-python-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/otomatikportal/hepsijet-unofficial-python-sdk/issues
|
|
8
|
+
Author-email: Omer <omer@novosdijital.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: pydantic[email]>=2.6
|
|
22
|
+
Requires-Dist: typing-extensions>=4.7
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# hepsijet
|
|
30
|
+
|
|
31
|
+
A typed, readable Python SDK for the [HepsiJET cargo API](https://developers.hepsiburada.com/tr/companies/hepsijet).
|
|
32
|
+
|
|
33
|
+
- **Sync and async clients** with identical methods: `HepsijetClient` for Django, `AsyncHepsijetClient` for FastAPI.
|
|
34
|
+
- **Typed models** (pydantic v2). Your editor autocompletes every field, and mistakes are caught when you build the model, before anything reaches HepsiJET.
|
|
35
|
+
- **Automatic tokens.** The client logs in, caches the token and refreshes it for you.
|
|
36
|
+
- **Python names, not wire names.** You write `Shipment(barcode=..., desi=3)`, and the SDK sends `customerDeliveryNo`, `deliverySlotOriginal` and the rest.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install ./hepsijet-sdk # or: pip install hepsijet-0.1.0-py3-none-any.whl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.10+.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 1. Configure once
|
|
47
|
+
|
|
48
|
+
HepsiJET gives you every value below except `warehouse`, which must match the warehouse
|
|
49
|
+
address you registered with them.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from hepsijet import Address, HepsijetConfig
|
|
53
|
+
|
|
54
|
+
config = HepsijetConfig(
|
|
55
|
+
username="...",
|
|
56
|
+
password="...",
|
|
57
|
+
company_name="KARLOG", # from HepsiJET
|
|
58
|
+
company_code="KRL", # from HepsiJET
|
|
59
|
+
xdock_code="KRL_IST", # from HepsiJET
|
|
60
|
+
warehouse=Address(
|
|
61
|
+
address_id="KRL_DEPO", # from HepsiJET
|
|
62
|
+
city="İstanbul",
|
|
63
|
+
town="Kartal",
|
|
64
|
+
district="Petrol İş",
|
|
65
|
+
line="Kamelya Sk. No:10",
|
|
66
|
+
),
|
|
67
|
+
# base_url="https://...", # live URL from HepsiJET. The default is the test server.
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 2. Everything the client can do
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from hepsijet import HepsijetClient
|
|
75
|
+
|
|
76
|
+
hepsijet = HepsijetClient(config) # create once, reuse everywhere (thread-safe)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Task | Call | Returns |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Create a shipment (auto-XL above 40 desi) | `hepsijet.shipments.create(shipment)` | `ApiResult` |
|
|
82
|
+
| Change a shipment before pickup | `hepsijet.shipments.update(change)` | `ApiResult` |
|
|
83
|
+
| Cancel a shipment before pickup | `hepsijet.shipments.cancel(barcode)` | `ApiResult` |
|
|
84
|
+
| Days available for a return pickup | `hepsijet.returns.pickup_days(city=, town=, start=, end=)` | `list[PickupDay]` |
|
|
85
|
+
| Create a scheduled return | `hepsijet.returns.create(return_shipment)` | `ApiResult` |
|
|
86
|
+
| Label as PDF | `hepsijet.labels.pdf(barcodes)` | `bytes` |
|
|
87
|
+
| Labels as PNG/JPEG | `hepsijet.labels.images(barcodes, ImageFormat.PNG)` | `list[bytes]` |
|
|
88
|
+
| Label for Zebra printers | `hepsijet.labels.zpl(barcode, parcel_count)` | `str` |
|
|
89
|
+
| Customer-facing status + link | `hepsijet.tracking.summary(barcodes)` | `list[TrackingSummary]` |
|
|
90
|
+
| Full scan history | `hepsijet.tracking.history(barcodes)` | `list[ShipmentHistory]` |
|
|
91
|
+
| Cities / towns / districts served | `hepsijet.service_areas.cities()` · `.towns(city_id)` · `.districts(town_id)` | `list[Area]` |
|
|
92
|
+
| Register a marketplace merchant | `hepsijet.merchants.register(merchant)` | `ApiResult` |
|
|
93
|
+
| Delete a marketplace merchant | `hepsijet.merchants.delete(merchant_id)` | `ApiResult` |
|
|
94
|
+
|
|
95
|
+
`barcodes` can be a single string or a list of strings.
|
|
96
|
+
|
|
97
|
+
### Create a shipment
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from hepsijet import Address, Customer, DeliverySlot, ProductCode, Shipment
|
|
101
|
+
|
|
102
|
+
shipment = Shipment(
|
|
103
|
+
barcode="KRL1000000001", # your unique number, 9 to 16 characters
|
|
104
|
+
customer=Customer(
|
|
105
|
+
customer_id="KRL-C-551",
|
|
106
|
+
first_name="Ayşe",
|
|
107
|
+
last_name="Yılmaz",
|
|
108
|
+
phone="5321112233",
|
|
109
|
+
email="ayse@example.com",
|
|
110
|
+
),
|
|
111
|
+
address=Address(
|
|
112
|
+
address_id="KRL-C-551",
|
|
113
|
+
city="Bursa",
|
|
114
|
+
town="Osmangazi",
|
|
115
|
+
district="Güneştepe",
|
|
116
|
+
line="771 Sk. No:1 D:5",
|
|
117
|
+
),
|
|
118
|
+
desi=3,
|
|
119
|
+
parcel_count=1,
|
|
120
|
+
)
|
|
121
|
+
hepsijet.shipments.create(shipment)
|
|
122
|
+
|
|
123
|
+
# Same-day, 13:00–18:00
|
|
124
|
+
Shipment(..., product=ProductCode.SAME_DAY, slot=DeliverySlot.AFTERNOON)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Invalid combinations fail right away, with a clear message:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
Shipment(..., product=ProductCode.SAME_DAY)
|
|
131
|
+
# ValidationError: Same-day and next-day delivery need a time slot: MORNING, AFTERNOON or EVENING.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Change or cancel
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from hepsijet import ParcelUpdate, DeliveryOptionUpdate, RecipientUpdate, ProductCode
|
|
138
|
+
|
|
139
|
+
hepsijet.shipments.update(ParcelUpdate(barcode="KRL1000000001", desi_per_parcel=[1, 5, 3]))
|
|
140
|
+
hepsijet.shipments.update(
|
|
141
|
+
DeliveryOptionUpdate(barcode="KRL1000000001", product=ProductCode.NEXT_DAY)
|
|
142
|
+
)
|
|
143
|
+
hepsijet.shipments.update(
|
|
144
|
+
RecipientUpdate(barcode="KRL1000000001", name="Ali Can", phone="5361112233")
|
|
145
|
+
)
|
|
146
|
+
# Also available: CustomerUpdate, AddressUpdate
|
|
147
|
+
|
|
148
|
+
hepsijet.shipments.cancel("KRL1000000001")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Labels
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
pdf_bytes = hepsijet.labels.pdf(["KRL1000000001", "KRL1000000002"])
|
|
155
|
+
zpl_code = hepsijet.labels.zpl("KRL1000000001", parcel_count=2)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Tracking
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
for parcel in hepsijet.tracking.summary(["KRL1000000001", "KRL1000000002"]):
|
|
162
|
+
print(
|
|
163
|
+
parcel.barcode,
|
|
164
|
+
parcel.latest.status if parcel.latest else "not picked up",
|
|
165
|
+
parcel.tracking_url,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
[history] = hepsijet.tracking.history("KRL1000000001")
|
|
169
|
+
for event in history.events:
|
|
170
|
+
print(event.happened_at, event.delivery_status, event.location, event.description)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Scheduled returns
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from datetime import date, timedelta
|
|
177
|
+
from hepsijet import ReturnShipment
|
|
178
|
+
|
|
179
|
+
days = hepsijet.returns.pickup_days(
|
|
180
|
+
city="İstanbul", town="Kartal", start=date.today(), end=date.today() + timedelta(days=7)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
hepsijet.returns.create(
|
|
184
|
+
ReturnShipment(
|
|
185
|
+
barcode="KRL-R-100000001",
|
|
186
|
+
customer=customer,
|
|
187
|
+
pickup_address=customer_address, # the SDK swaps sender and recipient for you
|
|
188
|
+
desi=2,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Errors
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from hepsijet import HepsijetError, ApiError, AuthenticationError
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
hepsijet.shipments.cancel("KRL1000000001")
|
|
200
|
+
except AuthenticationError:
|
|
201
|
+
... # wrong username or password
|
|
202
|
+
except ApiError as error:
|
|
203
|
+
print(error.status_code, error.body) # HepsiJET said no (e.g. already picked up)
|
|
204
|
+
except HepsijetError:
|
|
205
|
+
... # anything else from this SDK
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Invalid input raises `pydantic.ValidationError` when you build the model, before any request is sent.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 3. Django
|
|
213
|
+
|
|
214
|
+
Create the client once, in a module, and import it wherever you need it.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# shipping/hepsijet.py
|
|
218
|
+
from django.conf import settings
|
|
219
|
+
from hepsijet import Address, HepsijetClient, HepsijetConfig
|
|
220
|
+
|
|
221
|
+
hepsijet = HepsijetClient(
|
|
222
|
+
HepsijetConfig(
|
|
223
|
+
username=settings.HEPSIJET_USERNAME,
|
|
224
|
+
password=settings.HEPSIJET_PASSWORD,
|
|
225
|
+
company_name=settings.HEPSIJET_COMPANY_NAME,
|
|
226
|
+
company_code=settings.HEPSIJET_COMPANY_CODE,
|
|
227
|
+
xdock_code=settings.HEPSIJET_XDOCK_CODE,
|
|
228
|
+
warehouse=Address(**settings.HEPSIJET_WAREHOUSE),
|
|
229
|
+
base_url=settings.HEPSIJET_BASE_URL,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
# shipping/views.py
|
|
236
|
+
from django.http import HttpResponse, JsonResponse
|
|
237
|
+
from .hepsijet import hepsijet
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def label(request, barcode: str) -> HttpResponse:
|
|
241
|
+
return HttpResponse(hepsijet.labels.pdf(barcode), content_type="application/pdf")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def track(request, barcode: str) -> JsonResponse:
|
|
245
|
+
[parcel] = hepsijet.tracking.summary(barcode)
|
|
246
|
+
return JsonResponse(parcel.model_dump(mode="json"))
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
See [`examples/django_example.py`](examples/django_example.py) for creating a shipment from an order model.
|
|
250
|
+
|
|
251
|
+
## 4. FastAPI
|
|
252
|
+
|
|
253
|
+
Open the async client when the app starts and close it on shutdown.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from contextlib import asynccontextmanager
|
|
257
|
+
from typing import Annotated
|
|
258
|
+
|
|
259
|
+
from fastapi import Depends, FastAPI, Request, Response
|
|
260
|
+
from hepsijet import AsyncHepsijetClient, TrackingSummary
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@asynccontextmanager
|
|
264
|
+
async def lifespan(app: FastAPI):
|
|
265
|
+
async with AsyncHepsijetClient(load_config()) as client:
|
|
266
|
+
app.state.hepsijet = client
|
|
267
|
+
yield
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
app = FastAPI(lifespan=lifespan)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def get_hepsijet(request: Request) -> AsyncHepsijetClient:
|
|
274
|
+
return request.app.state.hepsijet
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
Hepsijet = Annotated[AsyncHepsijetClient, Depends(get_hepsijet)]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@app.get("/shipments/{barcode}/tracking")
|
|
281
|
+
async def track(barcode: str, hepsijet: Hepsijet) -> TrackingSummary:
|
|
282
|
+
[parcel] = await hepsijet.tracking.summary(barcode)
|
|
283
|
+
return parcel
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@app.get("/shipments/{barcode}/label.pdf")
|
|
287
|
+
async def label(barcode: str, hepsijet: Hepsijet) -> Response:
|
|
288
|
+
return Response(await hepsijet.labels.pdf(barcode), media_type="application/pdf")
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Because the models are pydantic, you can use them directly as FastAPI request and response bodies.
|
|
292
|
+
See [`examples/fastapi_example.py`](examples/fastapi_example.py).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 5. How the code is organised
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
src/hepsijet/
|
|
300
|
+
├── client.py HepsijetClient: the sync client and its resource groups
|
|
301
|
+
├── async_client.py AsyncHepsijetClient: the same, with async/await
|
|
302
|
+
├── config.py HepsijetConfig: your account settings
|
|
303
|
+
├── enums.py ProductCode, DeliverySlot, DeliveryType, ...
|
|
304
|
+
├── errors.py HepsijetError and subclasses
|
|
305
|
+
├── models/ What you send (Shipment, updates, ...) and what you get back
|
|
306
|
+
├── _endpoints.py Every endpoint: method, path, and how to read the reply ← mirrors the API docs
|
|
307
|
+
├── _payloads.py Python models → HepsiJET JSON bodies ← the only place wire field names live
|
|
308
|
+
└── _auth.py Token fetching and caching
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Both clients send the same `Endpoint` objects, so the sync and async behaviour can't drift
|
|
312
|
+
apart. To add a new endpoint, add a function to `_endpoints.py` and one method to each client.
|
|
313
|
+
|
|
314
|
+
## 6. Unverified details in HepsiJET's docs
|
|
315
|
+
|
|
316
|
+
HepsiJET's documentation leaves some things out. The SDK handles each one leniently, but
|
|
317
|
+
check these against the **test server** before going live:
|
|
318
|
+
|
|
319
|
+
1. **Reply shapes.** Most replies aren't documented. Response models keep every field they
|
|
320
|
+
don't declare in `model_extra`, so nothing is lost. `Area` (service areas) and the label
|
|
321
|
+
replies are the least certain.
|
|
322
|
+
2. **Token reply.** The SDK looks for the token at `data.token`, then `token`.
|
|
323
|
+
3. **`currentXDock` placement.** The docs show it at the top level for normal shipments and
|
|
324
|
+
inside `delivery` for XL ones. The SDK sends it at the top level for both.
|
|
325
|
+
4. **Merchant registration.** The docs' example body is a copy of the shipment example; the
|
|
326
|
+
SDK follows the field-by-field description instead.
|
|
327
|
+
|
|
328
|
+
## Development
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
pip install -e ".[dev]"
|
|
332
|
+
pytest
|
|
333
|
+
mypy
|
|
334
|
+
ruff check .
|
|
335
|
+
```
|
hepsijet-0.1.0/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# hepsijet
|
|
2
|
+
|
|
3
|
+
A typed, readable Python SDK for the [HepsiJET cargo API](https://developers.hepsiburada.com/tr/companies/hepsijet).
|
|
4
|
+
|
|
5
|
+
- **Sync and async clients** with identical methods: `HepsijetClient` for Django, `AsyncHepsijetClient` for FastAPI.
|
|
6
|
+
- **Typed models** (pydantic v2). Your editor autocompletes every field, and mistakes are caught when you build the model, before anything reaches HepsiJET.
|
|
7
|
+
- **Automatic tokens.** The client logs in, caches the token and refreshes it for you.
|
|
8
|
+
- **Python names, not wire names.** You write `Shipment(barcode=..., desi=3)`, and the SDK sends `customerDeliveryNo`, `deliverySlotOriginal` and the rest.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install ./hepsijet-sdk # or: pip install hepsijet-0.1.0-py3-none-any.whl
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Python 3.10+.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Configure once
|
|
19
|
+
|
|
20
|
+
HepsiJET gives you every value below except `warehouse`, which must match the warehouse
|
|
21
|
+
address you registered with them.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from hepsijet import Address, HepsijetConfig
|
|
25
|
+
|
|
26
|
+
config = HepsijetConfig(
|
|
27
|
+
username="...",
|
|
28
|
+
password="...",
|
|
29
|
+
company_name="KARLOG", # from HepsiJET
|
|
30
|
+
company_code="KRL", # from HepsiJET
|
|
31
|
+
xdock_code="KRL_IST", # from HepsiJET
|
|
32
|
+
warehouse=Address(
|
|
33
|
+
address_id="KRL_DEPO", # from HepsiJET
|
|
34
|
+
city="İstanbul",
|
|
35
|
+
town="Kartal",
|
|
36
|
+
district="Petrol İş",
|
|
37
|
+
line="Kamelya Sk. No:10",
|
|
38
|
+
),
|
|
39
|
+
# base_url="https://...", # live URL from HepsiJET. The default is the test server.
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 2. Everything the client can do
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from hepsijet import HepsijetClient
|
|
47
|
+
|
|
48
|
+
hepsijet = HepsijetClient(config) # create once, reuse everywhere (thread-safe)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Task | Call | Returns |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Create a shipment (auto-XL above 40 desi) | `hepsijet.shipments.create(shipment)` | `ApiResult` |
|
|
54
|
+
| Change a shipment before pickup | `hepsijet.shipments.update(change)` | `ApiResult` |
|
|
55
|
+
| Cancel a shipment before pickup | `hepsijet.shipments.cancel(barcode)` | `ApiResult` |
|
|
56
|
+
| Days available for a return pickup | `hepsijet.returns.pickup_days(city=, town=, start=, end=)` | `list[PickupDay]` |
|
|
57
|
+
| Create a scheduled return | `hepsijet.returns.create(return_shipment)` | `ApiResult` |
|
|
58
|
+
| Label as PDF | `hepsijet.labels.pdf(barcodes)` | `bytes` |
|
|
59
|
+
| Labels as PNG/JPEG | `hepsijet.labels.images(barcodes, ImageFormat.PNG)` | `list[bytes]` |
|
|
60
|
+
| Label for Zebra printers | `hepsijet.labels.zpl(barcode, parcel_count)` | `str` |
|
|
61
|
+
| Customer-facing status + link | `hepsijet.tracking.summary(barcodes)` | `list[TrackingSummary]` |
|
|
62
|
+
| Full scan history | `hepsijet.tracking.history(barcodes)` | `list[ShipmentHistory]` |
|
|
63
|
+
| Cities / towns / districts served | `hepsijet.service_areas.cities()` · `.towns(city_id)` · `.districts(town_id)` | `list[Area]` |
|
|
64
|
+
| Register a marketplace merchant | `hepsijet.merchants.register(merchant)` | `ApiResult` |
|
|
65
|
+
| Delete a marketplace merchant | `hepsijet.merchants.delete(merchant_id)` | `ApiResult` |
|
|
66
|
+
|
|
67
|
+
`barcodes` can be a single string or a list of strings.
|
|
68
|
+
|
|
69
|
+
### Create a shipment
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from hepsijet import Address, Customer, DeliverySlot, ProductCode, Shipment
|
|
73
|
+
|
|
74
|
+
shipment = Shipment(
|
|
75
|
+
barcode="KRL1000000001", # your unique number, 9 to 16 characters
|
|
76
|
+
customer=Customer(
|
|
77
|
+
customer_id="KRL-C-551",
|
|
78
|
+
first_name="Ayşe",
|
|
79
|
+
last_name="Yılmaz",
|
|
80
|
+
phone="5321112233",
|
|
81
|
+
email="ayse@example.com",
|
|
82
|
+
),
|
|
83
|
+
address=Address(
|
|
84
|
+
address_id="KRL-C-551",
|
|
85
|
+
city="Bursa",
|
|
86
|
+
town="Osmangazi",
|
|
87
|
+
district="Güneştepe",
|
|
88
|
+
line="771 Sk. No:1 D:5",
|
|
89
|
+
),
|
|
90
|
+
desi=3,
|
|
91
|
+
parcel_count=1,
|
|
92
|
+
)
|
|
93
|
+
hepsijet.shipments.create(shipment)
|
|
94
|
+
|
|
95
|
+
# Same-day, 13:00–18:00
|
|
96
|
+
Shipment(..., product=ProductCode.SAME_DAY, slot=DeliverySlot.AFTERNOON)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Invalid combinations fail right away, with a clear message:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
Shipment(..., product=ProductCode.SAME_DAY)
|
|
103
|
+
# ValidationError: Same-day and next-day delivery need a time slot: MORNING, AFTERNOON or EVENING.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Change or cancel
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from hepsijet import ParcelUpdate, DeliveryOptionUpdate, RecipientUpdate, ProductCode
|
|
110
|
+
|
|
111
|
+
hepsijet.shipments.update(ParcelUpdate(barcode="KRL1000000001", desi_per_parcel=[1, 5, 3]))
|
|
112
|
+
hepsijet.shipments.update(
|
|
113
|
+
DeliveryOptionUpdate(barcode="KRL1000000001", product=ProductCode.NEXT_DAY)
|
|
114
|
+
)
|
|
115
|
+
hepsijet.shipments.update(
|
|
116
|
+
RecipientUpdate(barcode="KRL1000000001", name="Ali Can", phone="5361112233")
|
|
117
|
+
)
|
|
118
|
+
# Also available: CustomerUpdate, AddressUpdate
|
|
119
|
+
|
|
120
|
+
hepsijet.shipments.cancel("KRL1000000001")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Labels
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
pdf_bytes = hepsijet.labels.pdf(["KRL1000000001", "KRL1000000002"])
|
|
127
|
+
zpl_code = hepsijet.labels.zpl("KRL1000000001", parcel_count=2)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Tracking
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
for parcel in hepsijet.tracking.summary(["KRL1000000001", "KRL1000000002"]):
|
|
134
|
+
print(
|
|
135
|
+
parcel.barcode,
|
|
136
|
+
parcel.latest.status if parcel.latest else "not picked up",
|
|
137
|
+
parcel.tracking_url,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
[history] = hepsijet.tracking.history("KRL1000000001")
|
|
141
|
+
for event in history.events:
|
|
142
|
+
print(event.happened_at, event.delivery_status, event.location, event.description)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Scheduled returns
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from datetime import date, timedelta
|
|
149
|
+
from hepsijet import ReturnShipment
|
|
150
|
+
|
|
151
|
+
days = hepsijet.returns.pickup_days(
|
|
152
|
+
city="İstanbul", town="Kartal", start=date.today(), end=date.today() + timedelta(days=7)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
hepsijet.returns.create(
|
|
156
|
+
ReturnShipment(
|
|
157
|
+
barcode="KRL-R-100000001",
|
|
158
|
+
customer=customer,
|
|
159
|
+
pickup_address=customer_address, # the SDK swaps sender and recipient for you
|
|
160
|
+
desi=2,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Errors
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from hepsijet import HepsijetError, ApiError, AuthenticationError
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
hepsijet.shipments.cancel("KRL1000000001")
|
|
172
|
+
except AuthenticationError:
|
|
173
|
+
... # wrong username or password
|
|
174
|
+
except ApiError as error:
|
|
175
|
+
print(error.status_code, error.body) # HepsiJET said no (e.g. already picked up)
|
|
176
|
+
except HepsijetError:
|
|
177
|
+
... # anything else from this SDK
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Invalid input raises `pydantic.ValidationError` when you build the model, before any request is sent.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 3. Django
|
|
185
|
+
|
|
186
|
+
Create the client once, in a module, and import it wherever you need it.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# shipping/hepsijet.py
|
|
190
|
+
from django.conf import settings
|
|
191
|
+
from hepsijet import Address, HepsijetClient, HepsijetConfig
|
|
192
|
+
|
|
193
|
+
hepsijet = HepsijetClient(
|
|
194
|
+
HepsijetConfig(
|
|
195
|
+
username=settings.HEPSIJET_USERNAME,
|
|
196
|
+
password=settings.HEPSIJET_PASSWORD,
|
|
197
|
+
company_name=settings.HEPSIJET_COMPANY_NAME,
|
|
198
|
+
company_code=settings.HEPSIJET_COMPANY_CODE,
|
|
199
|
+
xdock_code=settings.HEPSIJET_XDOCK_CODE,
|
|
200
|
+
warehouse=Address(**settings.HEPSIJET_WAREHOUSE),
|
|
201
|
+
base_url=settings.HEPSIJET_BASE_URL,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
# shipping/views.py
|
|
208
|
+
from django.http import HttpResponse, JsonResponse
|
|
209
|
+
from .hepsijet import hepsijet
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def label(request, barcode: str) -> HttpResponse:
|
|
213
|
+
return HttpResponse(hepsijet.labels.pdf(barcode), content_type="application/pdf")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def track(request, barcode: str) -> JsonResponse:
|
|
217
|
+
[parcel] = hepsijet.tracking.summary(barcode)
|
|
218
|
+
return JsonResponse(parcel.model_dump(mode="json"))
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [`examples/django_example.py`](examples/django_example.py) for creating a shipment from an order model.
|
|
222
|
+
|
|
223
|
+
## 4. FastAPI
|
|
224
|
+
|
|
225
|
+
Open the async client when the app starts and close it on shutdown.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from contextlib import asynccontextmanager
|
|
229
|
+
from typing import Annotated
|
|
230
|
+
|
|
231
|
+
from fastapi import Depends, FastAPI, Request, Response
|
|
232
|
+
from hepsijet import AsyncHepsijetClient, TrackingSummary
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@asynccontextmanager
|
|
236
|
+
async def lifespan(app: FastAPI):
|
|
237
|
+
async with AsyncHepsijetClient(load_config()) as client:
|
|
238
|
+
app.state.hepsijet = client
|
|
239
|
+
yield
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
app = FastAPI(lifespan=lifespan)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_hepsijet(request: Request) -> AsyncHepsijetClient:
|
|
246
|
+
return request.app.state.hepsijet
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
Hepsijet = Annotated[AsyncHepsijetClient, Depends(get_hepsijet)]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@app.get("/shipments/{barcode}/tracking")
|
|
253
|
+
async def track(barcode: str, hepsijet: Hepsijet) -> TrackingSummary:
|
|
254
|
+
[parcel] = await hepsijet.tracking.summary(barcode)
|
|
255
|
+
return parcel
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.get("/shipments/{barcode}/label.pdf")
|
|
259
|
+
async def label(barcode: str, hepsijet: Hepsijet) -> Response:
|
|
260
|
+
return Response(await hepsijet.labels.pdf(barcode), media_type="application/pdf")
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Because the models are pydantic, you can use them directly as FastAPI request and response bodies.
|
|
264
|
+
See [`examples/fastapi_example.py`](examples/fastapi_example.py).
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 5. How the code is organised
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
src/hepsijet/
|
|
272
|
+
├── client.py HepsijetClient: the sync client and its resource groups
|
|
273
|
+
├── async_client.py AsyncHepsijetClient: the same, with async/await
|
|
274
|
+
├── config.py HepsijetConfig: your account settings
|
|
275
|
+
├── enums.py ProductCode, DeliverySlot, DeliveryType, ...
|
|
276
|
+
├── errors.py HepsijetError and subclasses
|
|
277
|
+
├── models/ What you send (Shipment, updates, ...) and what you get back
|
|
278
|
+
├── _endpoints.py Every endpoint: method, path, and how to read the reply ← mirrors the API docs
|
|
279
|
+
├── _payloads.py Python models → HepsiJET JSON bodies ← the only place wire field names live
|
|
280
|
+
└── _auth.py Token fetching and caching
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Both clients send the same `Endpoint` objects, so the sync and async behaviour can't drift
|
|
284
|
+
apart. To add a new endpoint, add a function to `_endpoints.py` and one method to each client.
|
|
285
|
+
|
|
286
|
+
## 6. Unverified details in HepsiJET's docs
|
|
287
|
+
|
|
288
|
+
HepsiJET's documentation leaves some things out. The SDK handles each one leniently, but
|
|
289
|
+
check these against the **test server** before going live:
|
|
290
|
+
|
|
291
|
+
1. **Reply shapes.** Most replies aren't documented. Response models keep every field they
|
|
292
|
+
don't declare in `model_extra`, so nothing is lost. `Area` (service areas) and the label
|
|
293
|
+
replies are the least certain.
|
|
294
|
+
2. **Token reply.** The SDK looks for the token at `data.token`, then `token`.
|
|
295
|
+
3. **`currentXDock` placement.** The docs show it at the top level for normal shipments and
|
|
296
|
+
inside `delivery` for XL ones. The SDK sends it at the top level for both.
|
|
297
|
+
4. **Merchant registration.** The docs' example body is a copy of the shipment example; the
|
|
298
|
+
SDK follows the field-by-field description instead.
|
|
299
|
+
|
|
300
|
+
## Development
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
pip install -e ".[dev]"
|
|
304
|
+
pytest
|
|
305
|
+
mypy
|
|
306
|
+
ruff check .
|
|
307
|
+
```
|