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.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ .mypy_cache/
6
+ .pytest_cache/
7
+ .ruff_cache/
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.
@@ -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
+ ```
@@ -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
+ ```