superroute-slocker 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.
- superroute_slocker-0.1.0/LICENSE +21 -0
- superroute_slocker-0.1.0/PKG-INFO +326 -0
- superroute_slocker-0.1.0/README.md +310 -0
- superroute_slocker-0.1.0/pyproject.toml +28 -0
- superroute_slocker-0.1.0/setup.cfg +4 -0
- superroute_slocker-0.1.0/src/slocker/__init__.py +11 -0
- superroute_slocker-0.1.0/src/slocker/_operations.py +666 -0
- superroute_slocker-0.1.0/src/slocker/client.py +214 -0
- superroute_slocker-0.1.0/src/slocker/errors.py +31 -0
- superroute_slocker-0.1.0/src/slocker/py.typed +0 -0
- superroute_slocker-0.1.0/src/slocker/webhooks.py +146 -0
- superroute_slocker-0.1.0/src/superroute_slocker.egg-info/PKG-INFO +326 -0
- superroute_slocker-0.1.0/src/superroute_slocker.egg-info/SOURCES.txt +14 -0
- superroute_slocker-0.1.0/src/superroute_slocker.egg-info/dependency_links.txt +1 -0
- superroute_slocker-0.1.0/src/superroute_slocker.egg-info/top_level.txt +1 -0
- superroute_slocker-0.1.0/tests/test_shared.py +146 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Superroute SDK License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Superroute Inc. All rights reserved, except as granted below.
|
|
4
|
+
|
|
5
|
+
1. Permission is granted, free of charge, to any person obtaining a copy of this software and its documentation
|
|
6
|
+
(the "Software") to install, use, copy and distribute the Software, including as part of their own products and
|
|
7
|
+
services, to connect to Superroute services, on any other platform, and in their own commercial projects.
|
|
8
|
+
|
|
9
|
+
2. The Software may not be sold on its own, whether alone or as the main part of a product whose value lies in the
|
|
10
|
+
Software rather than in the product built with it.
|
|
11
|
+
|
|
12
|
+
3. The copyright notices, this license and the attribution of the Software to Superroute may not be removed or
|
|
13
|
+
changed, and must be kept in every copy or substantial portion of the Software that is distributed.
|
|
14
|
+
|
|
15
|
+
4. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
16
|
+
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
18
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
19
|
+
IN THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
5. Use of Superroute services through the Software is governed by the terms of those services.
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: superroute-slocker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: sLocker: deliver to Superroute smart lockers. The partner locker delivery API and its webhooks.
|
|
5
|
+
Author: Superroute
|
|
6
|
+
License-Expression: LicenseRef-Superroute-SDK-License
|
|
7
|
+
Project-URL: Homepage, https://open.superroute.ca/developer
|
|
8
|
+
Keywords: superroute,smart locker,parcel locker,delivery,webhooks
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Goodneo-Solutions/slocker-releases/main/assets/slocker-logo-dark.svg">
|
|
19
|
+
<img src="https://raw.githubusercontent.com/Goodneo-Solutions/slocker-releases/main/assets/slocker-logo.svg" alt="sLocker" height="56">
|
|
20
|
+
</picture>
|
|
21
|
+
|
|
22
|
+
# sLocker
|
|
23
|
+
|
|
24
|
+
Deliver to Superroute smart lockers. sLocker is the SDK for carriers and delivery partners that put parcels into
|
|
25
|
+
Superroute smart lockers: create a delivery task, open the doors remotely for the driver, confirm the deposit, follow
|
|
26
|
+
the pickup, and receive and verify the webhooks — with a mock cabinet to rehearse all of it.
|
|
27
|
+
Superroute SDK License — see [LICENSE](LICENSE) and [open.superroute.ca/developer/license](https://open.superroute.ca/developer/license).
|
|
28
|
+
|
|
29
|
+
| Package | Platform | Install |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `superroute-slocker` (PyPI) | Python 3.9+ | `pip install superroute-slocker` |
|
|
32
|
+
| `superroute/slocker` (Packagist) | PHP 8.1+ | `composer require superroute/slocker` |
|
|
33
|
+
| `ca.superroute:slocker` (Maven) | Java 11+ and Android | see [Java and Kotlin](#java-and-kotlin) |
|
|
34
|
+
| `ca.superroute:slocker-android` (Maven) | Kotlin coroutines and OkHttp, for Android | see [Java and Kotlin](#java-and-kotlin) |
|
|
35
|
+
| `github.com/Goodneo-Solutions/slocker-releases/go` | Go 1.21+ | `go get github.com/Goodneo-Solutions/slocker-releases/go` |
|
|
36
|
+
| `Superroute.SLocker` (NuGet) | .NET 8 | `dotnet add package Superroute.SLocker` |
|
|
37
|
+
| `@superrouteca/slocker` (npm) | Node 20+, browsers, Deno, Bun; TypeScript types | `npm install @superrouteca/slocker` |
|
|
38
|
+
| `SLocker` (Swift package) | iOS 15+, macOS 12+ | `https://github.com/Goodneo-Solutions/slocker-releases` |
|
|
39
|
+
| `slocker` (pub.dev) | Dart 3.5+ and Flutter | `dart pub add slocker` |
|
|
40
|
+
|
|
41
|
+
Every package of a release carries the same version and covers the same operations.
|
|
42
|
+
|
|
43
|
+
## What it covers
|
|
44
|
+
|
|
45
|
+
The partner locker delivery API of Superroute (`/api/v1/third-party-delivery/…`), 40 operations, each a method:
|
|
46
|
+
|
|
47
|
+
- **Delivery tasks** — create (idempotent), list, read, cancel, rotate a code.
|
|
48
|
+
- **At the cabinet** — open the doors remotely with the delivery code, confirm (all or a partial delivery), abort,
|
|
49
|
+
reassign a package to another compartment, cancel the placement, reopen within the 60-second correction window,
|
|
50
|
+
open for pickup.
|
|
51
|
+
- **Scopes and locations** — the grants and sub-scopes, create / update / disable a sub-scope, the locations of a
|
|
52
|
+
scope, the locations near an address or a point.
|
|
53
|
+
- **Usage** — summary, daily, by location.
|
|
54
|
+
- **Carriers** — the carrier profile, intake locations and receiving warehouses, pre-advising packages into a
|
|
55
|
+
warehouse, the carrier's packages, cancelling, rerouting, and resetting a pickup code.
|
|
56
|
+
- **Mock cabinet** — create, list and delete mock lockers, force doors open or closed, take a cabinet offline, change
|
|
57
|
+
its features and compartment sizes, seed parcels, clear them, or declare them taken.
|
|
58
|
+
|
|
59
|
+
The operations are generated from Superroute's published OpenAPI document (`specs/partner-locker-api.json`). The same
|
|
60
|
+
operations exist in Superroute's GraphQL API (`specs/partner-locker-graphql.json` lists them); sLocker calls REST.
|
|
61
|
+
|
|
62
|
+
## Signing in and choosing the provider
|
|
63
|
+
|
|
64
|
+
The API takes the bearer token of a Superroute API account (`POST /api/v1/user/login`; a token lasts a week). Pass a
|
|
65
|
+
token you already have, or sign in with the client, which keeps the token for the following calls:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { SLockerClient } from '@superrouteca/slocker';
|
|
69
|
+
|
|
70
|
+
const sl = new SLockerClient({ providerId: 4611 }); // baseUrl defaults to https://api.superroute.ca
|
|
71
|
+
await sl.login({ email: 'api-partner@example.com', password: process.env.SR_PASSWORD });
|
|
72
|
+
// later: await sl.refreshToken(); await sl.rotateToken({ gracePeriodMinutes: 60 }); await sl.logout();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`providerId` is your third-party delivery provider (sent as `X-Third-Party-Provider-Id`). It may be left out when the
|
|
76
|
+
account acts for exactly one provider; any call can name another one (`providerId` in its parameters).
|
|
77
|
+
|
|
78
|
+
## A delivery, end to end
|
|
79
|
+
|
|
80
|
+
1. **Create the task** with the order's own idempotency key. The answer carries `delivery_no`, the driver's
|
|
81
|
+
`access_code` and the recipient's `pickup_code`. A repeat with the same key and the same content answers the
|
|
82
|
+
original task (`replayed: true`); the same key with different content is `409 IDEMPOTENCY_CONFLICT`.
|
|
83
|
+
2. **At the cabinet**, the driver types the code on the screen — or your app opens the doors remotely with
|
|
84
|
+
`openPartnerLockerDelivery`, which answers a `claim_token` and the compartments that opened.
|
|
85
|
+
3. **Confirm** with the claim token (`confirm_all`, or `accept_partial` when some packages did not fit). For a remote
|
|
86
|
+
open the cabinet settles the deposit by itself once every opened door reports closed; an explicit confirm afterwards
|
|
87
|
+
answers the same result.
|
|
88
|
+
4. **Webhooks** tell you the rest: `partner_locker.delivery.doors_opened`, `.delivered` (with the pickup code and the
|
|
89
|
+
compartments), `partner_locker.pickup.completed`, `.failed`, `.expired`, `.cancelled`, `.correction_reopened`.
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const created = await sl.api.createPartnerLockerDelivery({
|
|
93
|
+
idempotencyKey: 'FOOD-20260714-1001-v1', // your own, stable for this order
|
|
94
|
+
body: {
|
|
95
|
+
external_order_id: 'FOOD-20260714-1001',
|
|
96
|
+
platform_scope_group_id: 12,
|
|
97
|
+
recipient: { phone: '+14165550123' },
|
|
98
|
+
package_count: 2,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const { delivery_no, access_code } = created.data;
|
|
102
|
+
|
|
103
|
+
const opened = await sl.api.openPartnerLockerDelivery({ body: { location_id: 101, access_code } });
|
|
104
|
+
const { claim_token, attempt } = opened.data;
|
|
105
|
+
if (attempt.can_confirm) {
|
|
106
|
+
await sl.api.confirmPartnerLockerDelivery({ body: { claim_token, decision: 'confirm_all' } });
|
|
107
|
+
}
|
|
108
|
+
const task = await sl.api.partnerLockerDelivery({ deliveryNo: delivery_no });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The same in Python:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from slocker import SLockerClient, SLockerError
|
|
115
|
+
|
|
116
|
+
sl = SLockerClient(provider_id=4611)
|
|
117
|
+
sl.login('api-partner@example.com', password)
|
|
118
|
+
created = sl.create_partner_locker_delivery(
|
|
119
|
+
idempotency_key='FOOD-20260714-1001-v1',
|
|
120
|
+
body={'external_order_id': 'FOOD-20260714-1001', 'platform_scope_group_id': 12,
|
|
121
|
+
'recipient': {'phone': '+14165550123'}, 'package_count': 2})
|
|
122
|
+
try:
|
|
123
|
+
opened = sl.open_partner_locker_delivery(body={'location_id': 101, 'access_code': created['data']['access_code']})
|
|
124
|
+
sl.confirm_partner_locker_delivery(body={'claim_token': opened['data']['claim_token'], 'decision': 'confirm_all'})
|
|
125
|
+
except SLockerError as e:
|
|
126
|
+
if e.code == 'LOCKER_DELIVERY_IN_USE':
|
|
127
|
+
... # another attempt is in progress: wait, or use its claim token
|
|
128
|
+
raise
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## The sandbox: a mock cabinet
|
|
132
|
+
|
|
133
|
+
A mock cabinet runs the same pipeline as a real one without hardware; a task created with `mock: true` only uses mock
|
|
134
|
+
cabinets, and its webhooks carry `livemode: false`, an `event_id` starting `PLE-MOCK-` and the header `X-Webhook-Test:
|
|
135
|
+
1` (sent to your sandbox URL when one is set). Up to 5 mock cabinets per provider, 1–60 compartments each.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
locker = sl.create_partner_locker_mock_locker(body={'location_id': 101, 'name': 'Dev cabinet', 'compartments': 8})
|
|
139
|
+
device_id = locker['data']['locker']['device_id']
|
|
140
|
+
|
|
141
|
+
task = sl.create_partner_locker_delivery(idempotency_key='TEST-1', body={
|
|
142
|
+
'external_order_id': 'TEST-1', 'platform_scope_group_id': 12, 'recipient': {'email': 'qa@example.com'}, 'mock': True})
|
|
143
|
+
opened = sl.open_partner_locker_delivery(body={'location_id': 101, 'access_code': task['data']['access_code']})
|
|
144
|
+
sl.confirm_partner_locker_delivery(body={'claim_token': opened['data']['claim_token'], 'decision': 'confirm_all'})
|
|
145
|
+
# → webhook partner_locker.delivery.delivered, livemode false
|
|
146
|
+
|
|
147
|
+
sl.mark_partner_locker_mock_parcels_taken(device_id, body={'compartments': [1]})
|
|
148
|
+
# → webhook partner_locker.pickup.completed (a full pickup settlement)
|
|
149
|
+
|
|
150
|
+
sl.clear_partner_locker_mock_parcels(device_id) # empties the cabinet without a pickup settlement
|
|
151
|
+
sl.set_partner_locker_mock_door(device_id, body={'compartment': 2, 'action': 'open'}) # a door left open
|
|
152
|
+
sl.set_partner_locker_mock_compartments(device_id, body={'compartments': [1, 2], 'grid_length_cm': 20,
|
|
153
|
+
'grid_width_cm': 20, 'grid_height_cm': 20, 'weight_limit_kg': 5}) # rehearse "does not fit"
|
|
154
|
+
sl.delete_partner_locker_mock_locker(device_id)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use the location list in the same mode as the task (`mock: true` on `partnerLockerScopeLocations` /
|
|
158
|
+
`partnerLockerNearbyLocations`): a live location for a mock task answers `409 LOCKER_NO_CABINET_FOR_MODE`.
|
|
159
|
+
|
|
160
|
+
## Webhooks
|
|
161
|
+
|
|
162
|
+
Verify every webhook against the **raw body** exactly as received, before parsing it. sLocker checks the timestamp
|
|
163
|
+
(±5 minutes by default), the `X-Webhook-Content-Digest` and the `X-Webhook-Signature`
|
|
164
|
+
(`v1=` base64 HMAC-SHA256 of `{timestamp}\n{X-Webhook-Delivery-Id}\n{body}` with your endpoint's signing secret), and
|
|
165
|
+
answers the event. During a key rotation the previous secret stays valid for 24 hours: pass both. An endpoint switched
|
|
166
|
+
to Standard Webhooks (`webhook-id` / `webhook-timestamp` / `webhook-signature`) or to CloudEvents structured bodies is
|
|
167
|
+
verified too. The same verification covers the carrier channel's `carrier_package.distribution.*` events.
|
|
168
|
+
|
|
169
|
+
Deduplicate on `eventId` (stable across retries; `deliveryId` changes on every attempt), answer 2xx within 10 seconds,
|
|
170
|
+
and keep sandbox events (`event.test`) apart from production.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { verifyWebhook, WebhookVerificationError } from '@superrouteca/slocker';
|
|
174
|
+
|
|
175
|
+
app.post('/webhooks/slocker', express.raw({ type: '*/*' }), async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const event = await verifyWebhook(req.body, req.headers, [process.env.SLOCKER_SECRET, process.env.SLOCKER_OLD_SECRET]);
|
|
178
|
+
if (!event.test) await queue.add(event.eventId, event);
|
|
179
|
+
res.sendStatus(204);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
if (e instanceof WebhookVerificationError) return res.status(400).send(e.reason);
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
| Language | Verify | Sign a test request |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| Python | `slocker.verify(body, headers, secrets)` | `slocker.sign(secret, body)` |
|
|
190
|
+
| PHP | `Webhooks::verify(file_get_contents('php://input'), getallheaders(), $secrets)` | `Webhooks::sign($secret, $body)` |
|
|
191
|
+
| Java | `Webhooks.verify(body, headers, secrets)` | `Webhooks.sign(...)` |
|
|
192
|
+
| Go | `slocker.Verify(body, slocker.HeaderMap(r.Header), secrets, nil)` | `slocker.Sign(secret, body, slocker.SignOptions{})` |
|
|
193
|
+
| C# | `Webhooks.Verify(body, headers, secrets)` | `Webhooks.Sign(secret, body)` |
|
|
194
|
+
| JavaScript | `await verifyWebhook(body, headers, secrets)` | `await signWebhook(secret, body)` |
|
|
195
|
+
| Swift | `try Webhooks.verify(body:headers:secrets:)` | `Webhooks.sign(secret:body:)` |
|
|
196
|
+
| Dart | `verifyWebhook(bodyBytes, headers, secrets)` | `signWebhook(secret, bodyBytes)` |
|
|
197
|
+
|
|
198
|
+
A refused webhook raises an error whose reason is one of `MISSING_HEADER`, `INVALID_TIMESTAMP`,
|
|
199
|
+
`TIMESTAMP_OUT_OF_TOLERANCE`, `DIGEST_MISMATCH`, `SIGNATURE_MISMATCH` or `INVALID_PAYLOAD`.
|
|
200
|
+
|
|
201
|
+
## Errors and retries
|
|
202
|
+
|
|
203
|
+
Every failed call raises one error type with the HTTP `status`, the server's stable `code`, its `message`, the field
|
|
204
|
+
`errors` and `retryAfter` (seconds). Act on the status and the code, never on the message. Codes the SDK adds:
|
|
205
|
+
`UNAUTHENTICATED` (401 without a code: sign in again), `HTTP_<status>` (an answer without a code), `NETWORK_ERROR`
|
|
206
|
+
(no answer; status 0) and `INVALID_ARGUMENT` (a missing path parameter or an unknown operation; nothing was sent).
|
|
207
|
+
|
|
208
|
+
Only repeatable calls are retried: reads, deletes, and the two creates that carry an idempotency key
|
|
209
|
+
(`createPartnerLockerDelivery`, `submitCarrierPackages`). They are repeated after a timeout, a lost connection, 408,
|
|
210
|
+
425, 429 or 5xx — at most `maxRetries` times (2 by default), waiting what `Retry-After` asks (at most 60 seconds) or
|
|
211
|
+
an exponential backoff with jitter. Opening doors, confirming, aborting, reassigning and every other change are never
|
|
212
|
+
repeated by the SDK: after a timeout, read the task (`partnerLockerDelivery`) before deciding.
|
|
213
|
+
|
|
214
|
+
When the SDK makes the idempotency key itself it is kept across its own retries only; give your own key (one per
|
|
215
|
+
order, unchanged on every retry of yours) so that a retry after a crash finds the original task.
|
|
216
|
+
|
|
217
|
+
## Each language
|
|
218
|
+
|
|
219
|
+
### Python
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from slocker import SLockerClient
|
|
223
|
+
sl = SLockerClient(token, provider_id=4611) # sync; urllib from the standard library
|
|
224
|
+
sl.partner_locker_deliveries(status='delivered', per_page=50)
|
|
225
|
+
sl.call('partnerLockerDeliveries', {'status': 'delivered'}) # any operation by name
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### PHP
|
|
229
|
+
|
|
230
|
+
```php
|
|
231
|
+
use Superroute\SLocker\Client;
|
|
232
|
+
$sl = new Client($token, providerId: 4611); // curl
|
|
233
|
+
$sl->partnerLockerDeliveries(status: 'delivered', perPage: 50);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Java and Kotlin
|
|
237
|
+
|
|
238
|
+
Add the Maven repository `https://goodneo-solutions.github.io/slocker-releases/maven`, then
|
|
239
|
+
`implementation("ca.superroute:slocker:0.1.0")` — or, in an Android app, `ca.superroute:slocker-android:0.1.0`.
|
|
240
|
+
|
|
241
|
+
```java
|
|
242
|
+
Client sl = Client.builder().token(token).providerId(4611L).build(); // java.net.http, or HttpURLConnection on Android
|
|
243
|
+
Object page = sl.partnerLockerDeliveries(Params.create().put("status", "delivered").put("per_page", 50));
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```kotlin
|
|
247
|
+
val sl = SLockerAsync(Client.builder().token(token).providerId(4611L).transport(OkHttpTransport()).build())
|
|
248
|
+
val page = sl.api.partnerLockerDeliveries(status = "delivered", perPage = 50) // suspend, on Dispatchers.IO
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Answers are plain JSON values (`Map`, `List`, `String`, `Long`, `Double`, `Boolean`, `null`).
|
|
252
|
+
|
|
253
|
+
### Go
|
|
254
|
+
|
|
255
|
+
```go
|
|
256
|
+
sl := slocker.New(slocker.Config{Token: token, ProviderID: 4611})
|
|
257
|
+
page, err := sl.PartnerLockerDeliveries(ctx, slocker.Params{"status": "delivered", "per_page": 50})
|
|
258
|
+
var e *slocker.Error
|
|
259
|
+
if errors.As(err, &e) && e.Code == "LOCKER_RATE_LIMITED" { /* … */ }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### C#
|
|
263
|
+
|
|
264
|
+
```csharp
|
|
265
|
+
var sl = new SLockerClient(new SLockerOptions { Token = token, ProviderId = 4611 });
|
|
266
|
+
var page = await sl.PartnerLockerDeliveriesAsync(status: "delivered", perPage: 50);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Swift
|
|
270
|
+
|
|
271
|
+
```swift
|
|
272
|
+
let sl = SLockerClient(token: token, providerId: 4611)
|
|
273
|
+
let page = try await sl.partnerLockerDeliveries(status: "delivered", perPage: 50)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Dart and Flutter
|
|
277
|
+
|
|
278
|
+
```dart
|
|
279
|
+
final sl = SLockerClient(token: token, providerId: 4611);
|
|
280
|
+
final page = await sl.partnerLockerDeliveries(status: 'delivered', perPage: 50);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Every client takes its own transport (for tests, proxies or an HTTP stack the app already has), a base URL (white-label
|
|
284
|
+
API hosts and test environments), the retry count and the wait function.
|
|
285
|
+
|
|
286
|
+
## Development
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
specs/ the partner locker slice of Superroute's REST and GraphQL documents, and operations.json
|
|
290
|
+
tools/sync_specs.py copies the slice from a Superroute checkout (and checks no route lacks its Swagger entry)
|
|
291
|
+
tools/generate.py writes every language's operation table and typed methods from the spec
|
|
292
|
+
tools/make_client_cases.py writes tests/cases/client.json: requests, answers, retries, sign-in
|
|
293
|
+
tools/make_webhook_vectors.py writes tests/cases/webhooks.json, signed exactly as Superroute signs
|
|
294
|
+
tests/cases/ the cases every language runs
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
python3 -I tools/sync_specs.py ../Superroute # then commit specs/ and the generated files
|
|
299
|
+
cd python && python3 -m unittest discover -s tests
|
|
300
|
+
cd js && npm test
|
|
301
|
+
cd php && php tests/run.php
|
|
302
|
+
cd go && go test ./...
|
|
303
|
+
cd java && ./gradlew :slocker:test :slocker-android:test
|
|
304
|
+
dotnet test dotnet/tests/Superroute.SLocker.Tests
|
|
305
|
+
cd swift && swift test
|
|
306
|
+
cd dart && dart pub get && dart test
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Never edit generated files (`generated/`, `Generated/`, `_operations.py`, `operations_gen.go`, `SLockerOperations.kt`):
|
|
310
|
+
change Superroute's spec, sync, and generate again.
|
|
311
|
+
|
|
312
|
+
## Releases
|
|
313
|
+
|
|
314
|
+
sLocker started at 0.1.0; each release adds 0.0.1 to the last one, skipping every version that contains the digit 3
|
|
315
|
+
or 4 (0.1.0, 0.1.1, 0.1.2, 0.1.5, 0.1.6, …, 0.1.9, 0.2.0, …). Tagging a version (`0.1.1`, no `v`) runs
|
|
316
|
+
`.github/workflows/release.yml`, which tests every language and then publishes:
|
|
317
|
+
|
|
318
|
+
- npm (`@superrouteca/slocker`), PyPI (`superroute-slocker`), NuGet (`Superroute.SLocker`) and pub.dev (`slocker`)
|
|
319
|
+
directly, by trusted publishing;
|
|
320
|
+
- to the public repository `Goodneo-Solutions/slocker-releases`: the Swift package (a prebuilt XCFramework attached to
|
|
321
|
+
that repository's GitHub Release of the version), the Maven repository (`maven/`, served by GitHub Pages) with
|
|
322
|
+
`ca.superroute:slocker` and `slocker-android`, and the Go module (`go/`, tag `go/v<version>`);
|
|
323
|
+
- to the public repository `Goodneo-Solutions/slocker-php`: the PHP package at its root, tag `v<version>`, which
|
|
324
|
+
Packagist reads.
|
|
325
|
+
|
|
326
|
+
This source repository stays private; the packages are public under the Superroute SDK License (see [LICENSE](LICENSE)).
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Goodneo-Solutions/slocker-releases/main/assets/slocker-logo-dark.svg">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Goodneo-Solutions/slocker-releases/main/assets/slocker-logo.svg" alt="sLocker" height="56">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
6
|
+
# sLocker
|
|
7
|
+
|
|
8
|
+
Deliver to Superroute smart lockers. sLocker is the SDK for carriers and delivery partners that put parcels into
|
|
9
|
+
Superroute smart lockers: create a delivery task, open the doors remotely for the driver, confirm the deposit, follow
|
|
10
|
+
the pickup, and receive and verify the webhooks — with a mock cabinet to rehearse all of it.
|
|
11
|
+
Superroute SDK License — see [LICENSE](LICENSE) and [open.superroute.ca/developer/license](https://open.superroute.ca/developer/license).
|
|
12
|
+
|
|
13
|
+
| Package | Platform | Install |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `superroute-slocker` (PyPI) | Python 3.9+ | `pip install superroute-slocker` |
|
|
16
|
+
| `superroute/slocker` (Packagist) | PHP 8.1+ | `composer require superroute/slocker` |
|
|
17
|
+
| `ca.superroute:slocker` (Maven) | Java 11+ and Android | see [Java and Kotlin](#java-and-kotlin) |
|
|
18
|
+
| `ca.superroute:slocker-android` (Maven) | Kotlin coroutines and OkHttp, for Android | see [Java and Kotlin](#java-and-kotlin) |
|
|
19
|
+
| `github.com/Goodneo-Solutions/slocker-releases/go` | Go 1.21+ | `go get github.com/Goodneo-Solutions/slocker-releases/go` |
|
|
20
|
+
| `Superroute.SLocker` (NuGet) | .NET 8 | `dotnet add package Superroute.SLocker` |
|
|
21
|
+
| `@superrouteca/slocker` (npm) | Node 20+, browsers, Deno, Bun; TypeScript types | `npm install @superrouteca/slocker` |
|
|
22
|
+
| `SLocker` (Swift package) | iOS 15+, macOS 12+ | `https://github.com/Goodneo-Solutions/slocker-releases` |
|
|
23
|
+
| `slocker` (pub.dev) | Dart 3.5+ and Flutter | `dart pub add slocker` |
|
|
24
|
+
|
|
25
|
+
Every package of a release carries the same version and covers the same operations.
|
|
26
|
+
|
|
27
|
+
## What it covers
|
|
28
|
+
|
|
29
|
+
The partner locker delivery API of Superroute (`/api/v1/third-party-delivery/…`), 40 operations, each a method:
|
|
30
|
+
|
|
31
|
+
- **Delivery tasks** — create (idempotent), list, read, cancel, rotate a code.
|
|
32
|
+
- **At the cabinet** — open the doors remotely with the delivery code, confirm (all or a partial delivery), abort,
|
|
33
|
+
reassign a package to another compartment, cancel the placement, reopen within the 60-second correction window,
|
|
34
|
+
open for pickup.
|
|
35
|
+
- **Scopes and locations** — the grants and sub-scopes, create / update / disable a sub-scope, the locations of a
|
|
36
|
+
scope, the locations near an address or a point.
|
|
37
|
+
- **Usage** — summary, daily, by location.
|
|
38
|
+
- **Carriers** — the carrier profile, intake locations and receiving warehouses, pre-advising packages into a
|
|
39
|
+
warehouse, the carrier's packages, cancelling, rerouting, and resetting a pickup code.
|
|
40
|
+
- **Mock cabinet** — create, list and delete mock lockers, force doors open or closed, take a cabinet offline, change
|
|
41
|
+
its features and compartment sizes, seed parcels, clear them, or declare them taken.
|
|
42
|
+
|
|
43
|
+
The operations are generated from Superroute's published OpenAPI document (`specs/partner-locker-api.json`). The same
|
|
44
|
+
operations exist in Superroute's GraphQL API (`specs/partner-locker-graphql.json` lists them); sLocker calls REST.
|
|
45
|
+
|
|
46
|
+
## Signing in and choosing the provider
|
|
47
|
+
|
|
48
|
+
The API takes the bearer token of a Superroute API account (`POST /api/v1/user/login`; a token lasts a week). Pass a
|
|
49
|
+
token you already have, or sign in with the client, which keeps the token for the following calls:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { SLockerClient } from '@superrouteca/slocker';
|
|
53
|
+
|
|
54
|
+
const sl = new SLockerClient({ providerId: 4611 }); // baseUrl defaults to https://api.superroute.ca
|
|
55
|
+
await sl.login({ email: 'api-partner@example.com', password: process.env.SR_PASSWORD });
|
|
56
|
+
// later: await sl.refreshToken(); await sl.rotateToken({ gracePeriodMinutes: 60 }); await sl.logout();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`providerId` is your third-party delivery provider (sent as `X-Third-Party-Provider-Id`). It may be left out when the
|
|
60
|
+
account acts for exactly one provider; any call can name another one (`providerId` in its parameters).
|
|
61
|
+
|
|
62
|
+
## A delivery, end to end
|
|
63
|
+
|
|
64
|
+
1. **Create the task** with the order's own idempotency key. The answer carries `delivery_no`, the driver's
|
|
65
|
+
`access_code` and the recipient's `pickup_code`. A repeat with the same key and the same content answers the
|
|
66
|
+
original task (`replayed: true`); the same key with different content is `409 IDEMPOTENCY_CONFLICT`.
|
|
67
|
+
2. **At the cabinet**, the driver types the code on the screen — or your app opens the doors remotely with
|
|
68
|
+
`openPartnerLockerDelivery`, which answers a `claim_token` and the compartments that opened.
|
|
69
|
+
3. **Confirm** with the claim token (`confirm_all`, or `accept_partial` when some packages did not fit). For a remote
|
|
70
|
+
open the cabinet settles the deposit by itself once every opened door reports closed; an explicit confirm afterwards
|
|
71
|
+
answers the same result.
|
|
72
|
+
4. **Webhooks** tell you the rest: `partner_locker.delivery.doors_opened`, `.delivered` (with the pickup code and the
|
|
73
|
+
compartments), `partner_locker.pickup.completed`, `.failed`, `.expired`, `.cancelled`, `.correction_reopened`.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const created = await sl.api.createPartnerLockerDelivery({
|
|
77
|
+
idempotencyKey: 'FOOD-20260714-1001-v1', // your own, stable for this order
|
|
78
|
+
body: {
|
|
79
|
+
external_order_id: 'FOOD-20260714-1001',
|
|
80
|
+
platform_scope_group_id: 12,
|
|
81
|
+
recipient: { phone: '+14165550123' },
|
|
82
|
+
package_count: 2,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const { delivery_no, access_code } = created.data;
|
|
86
|
+
|
|
87
|
+
const opened = await sl.api.openPartnerLockerDelivery({ body: { location_id: 101, access_code } });
|
|
88
|
+
const { claim_token, attempt } = opened.data;
|
|
89
|
+
if (attempt.can_confirm) {
|
|
90
|
+
await sl.api.confirmPartnerLockerDelivery({ body: { claim_token, decision: 'confirm_all' } });
|
|
91
|
+
}
|
|
92
|
+
const task = await sl.api.partnerLockerDelivery({ deliveryNo: delivery_no });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The same in Python:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from slocker import SLockerClient, SLockerError
|
|
99
|
+
|
|
100
|
+
sl = SLockerClient(provider_id=4611)
|
|
101
|
+
sl.login('api-partner@example.com', password)
|
|
102
|
+
created = sl.create_partner_locker_delivery(
|
|
103
|
+
idempotency_key='FOOD-20260714-1001-v1',
|
|
104
|
+
body={'external_order_id': 'FOOD-20260714-1001', 'platform_scope_group_id': 12,
|
|
105
|
+
'recipient': {'phone': '+14165550123'}, 'package_count': 2})
|
|
106
|
+
try:
|
|
107
|
+
opened = sl.open_partner_locker_delivery(body={'location_id': 101, 'access_code': created['data']['access_code']})
|
|
108
|
+
sl.confirm_partner_locker_delivery(body={'claim_token': opened['data']['claim_token'], 'decision': 'confirm_all'})
|
|
109
|
+
except SLockerError as e:
|
|
110
|
+
if e.code == 'LOCKER_DELIVERY_IN_USE':
|
|
111
|
+
... # another attempt is in progress: wait, or use its claim token
|
|
112
|
+
raise
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## The sandbox: a mock cabinet
|
|
116
|
+
|
|
117
|
+
A mock cabinet runs the same pipeline as a real one without hardware; a task created with `mock: true` only uses mock
|
|
118
|
+
cabinets, and its webhooks carry `livemode: false`, an `event_id` starting `PLE-MOCK-` and the header `X-Webhook-Test:
|
|
119
|
+
1` (sent to your sandbox URL when one is set). Up to 5 mock cabinets per provider, 1–60 compartments each.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
locker = sl.create_partner_locker_mock_locker(body={'location_id': 101, 'name': 'Dev cabinet', 'compartments': 8})
|
|
123
|
+
device_id = locker['data']['locker']['device_id']
|
|
124
|
+
|
|
125
|
+
task = sl.create_partner_locker_delivery(idempotency_key='TEST-1', body={
|
|
126
|
+
'external_order_id': 'TEST-1', 'platform_scope_group_id': 12, 'recipient': {'email': 'qa@example.com'}, 'mock': True})
|
|
127
|
+
opened = sl.open_partner_locker_delivery(body={'location_id': 101, 'access_code': task['data']['access_code']})
|
|
128
|
+
sl.confirm_partner_locker_delivery(body={'claim_token': opened['data']['claim_token'], 'decision': 'confirm_all'})
|
|
129
|
+
# → webhook partner_locker.delivery.delivered, livemode false
|
|
130
|
+
|
|
131
|
+
sl.mark_partner_locker_mock_parcels_taken(device_id, body={'compartments': [1]})
|
|
132
|
+
# → webhook partner_locker.pickup.completed (a full pickup settlement)
|
|
133
|
+
|
|
134
|
+
sl.clear_partner_locker_mock_parcels(device_id) # empties the cabinet without a pickup settlement
|
|
135
|
+
sl.set_partner_locker_mock_door(device_id, body={'compartment': 2, 'action': 'open'}) # a door left open
|
|
136
|
+
sl.set_partner_locker_mock_compartments(device_id, body={'compartments': [1, 2], 'grid_length_cm': 20,
|
|
137
|
+
'grid_width_cm': 20, 'grid_height_cm': 20, 'weight_limit_kg': 5}) # rehearse "does not fit"
|
|
138
|
+
sl.delete_partner_locker_mock_locker(device_id)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use the location list in the same mode as the task (`mock: true` on `partnerLockerScopeLocations` /
|
|
142
|
+
`partnerLockerNearbyLocations`): a live location for a mock task answers `409 LOCKER_NO_CABINET_FOR_MODE`.
|
|
143
|
+
|
|
144
|
+
## Webhooks
|
|
145
|
+
|
|
146
|
+
Verify every webhook against the **raw body** exactly as received, before parsing it. sLocker checks the timestamp
|
|
147
|
+
(±5 minutes by default), the `X-Webhook-Content-Digest` and the `X-Webhook-Signature`
|
|
148
|
+
(`v1=` base64 HMAC-SHA256 of `{timestamp}\n{X-Webhook-Delivery-Id}\n{body}` with your endpoint's signing secret), and
|
|
149
|
+
answers the event. During a key rotation the previous secret stays valid for 24 hours: pass both. An endpoint switched
|
|
150
|
+
to Standard Webhooks (`webhook-id` / `webhook-timestamp` / `webhook-signature`) or to CloudEvents structured bodies is
|
|
151
|
+
verified too. The same verification covers the carrier channel's `carrier_package.distribution.*` events.
|
|
152
|
+
|
|
153
|
+
Deduplicate on `eventId` (stable across retries; `deliveryId` changes on every attempt), answer 2xx within 10 seconds,
|
|
154
|
+
and keep sandbox events (`event.test`) apart from production.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { verifyWebhook, WebhookVerificationError } from '@superrouteca/slocker';
|
|
158
|
+
|
|
159
|
+
app.post('/webhooks/slocker', express.raw({ type: '*/*' }), async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const event = await verifyWebhook(req.body, req.headers, [process.env.SLOCKER_SECRET, process.env.SLOCKER_OLD_SECRET]);
|
|
162
|
+
if (!event.test) await queue.add(event.eventId, event);
|
|
163
|
+
res.sendStatus(204);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
if (e instanceof WebhookVerificationError) return res.status(400).send(e.reason);
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Language | Verify | Sign a test request |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| Python | `slocker.verify(body, headers, secrets)` | `slocker.sign(secret, body)` |
|
|
174
|
+
| PHP | `Webhooks::verify(file_get_contents('php://input'), getallheaders(), $secrets)` | `Webhooks::sign($secret, $body)` |
|
|
175
|
+
| Java | `Webhooks.verify(body, headers, secrets)` | `Webhooks.sign(...)` |
|
|
176
|
+
| Go | `slocker.Verify(body, slocker.HeaderMap(r.Header), secrets, nil)` | `slocker.Sign(secret, body, slocker.SignOptions{})` |
|
|
177
|
+
| C# | `Webhooks.Verify(body, headers, secrets)` | `Webhooks.Sign(secret, body)` |
|
|
178
|
+
| JavaScript | `await verifyWebhook(body, headers, secrets)` | `await signWebhook(secret, body)` |
|
|
179
|
+
| Swift | `try Webhooks.verify(body:headers:secrets:)` | `Webhooks.sign(secret:body:)` |
|
|
180
|
+
| Dart | `verifyWebhook(bodyBytes, headers, secrets)` | `signWebhook(secret, bodyBytes)` |
|
|
181
|
+
|
|
182
|
+
A refused webhook raises an error whose reason is one of `MISSING_HEADER`, `INVALID_TIMESTAMP`,
|
|
183
|
+
`TIMESTAMP_OUT_OF_TOLERANCE`, `DIGEST_MISMATCH`, `SIGNATURE_MISMATCH` or `INVALID_PAYLOAD`.
|
|
184
|
+
|
|
185
|
+
## Errors and retries
|
|
186
|
+
|
|
187
|
+
Every failed call raises one error type with the HTTP `status`, the server's stable `code`, its `message`, the field
|
|
188
|
+
`errors` and `retryAfter` (seconds). Act on the status and the code, never on the message. Codes the SDK adds:
|
|
189
|
+
`UNAUTHENTICATED` (401 without a code: sign in again), `HTTP_<status>` (an answer without a code), `NETWORK_ERROR`
|
|
190
|
+
(no answer; status 0) and `INVALID_ARGUMENT` (a missing path parameter or an unknown operation; nothing was sent).
|
|
191
|
+
|
|
192
|
+
Only repeatable calls are retried: reads, deletes, and the two creates that carry an idempotency key
|
|
193
|
+
(`createPartnerLockerDelivery`, `submitCarrierPackages`). They are repeated after a timeout, a lost connection, 408,
|
|
194
|
+
425, 429 or 5xx — at most `maxRetries` times (2 by default), waiting what `Retry-After` asks (at most 60 seconds) or
|
|
195
|
+
an exponential backoff with jitter. Opening doors, confirming, aborting, reassigning and every other change are never
|
|
196
|
+
repeated by the SDK: after a timeout, read the task (`partnerLockerDelivery`) before deciding.
|
|
197
|
+
|
|
198
|
+
When the SDK makes the idempotency key itself it is kept across its own retries only; give your own key (one per
|
|
199
|
+
order, unchanged on every retry of yours) so that a retry after a crash finds the original task.
|
|
200
|
+
|
|
201
|
+
## Each language
|
|
202
|
+
|
|
203
|
+
### Python
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from slocker import SLockerClient
|
|
207
|
+
sl = SLockerClient(token, provider_id=4611) # sync; urllib from the standard library
|
|
208
|
+
sl.partner_locker_deliveries(status='delivered', per_page=50)
|
|
209
|
+
sl.call('partnerLockerDeliveries', {'status': 'delivered'}) # any operation by name
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### PHP
|
|
213
|
+
|
|
214
|
+
```php
|
|
215
|
+
use Superroute\SLocker\Client;
|
|
216
|
+
$sl = new Client($token, providerId: 4611); // curl
|
|
217
|
+
$sl->partnerLockerDeliveries(status: 'delivered', perPage: 50);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Java and Kotlin
|
|
221
|
+
|
|
222
|
+
Add the Maven repository `https://goodneo-solutions.github.io/slocker-releases/maven`, then
|
|
223
|
+
`implementation("ca.superroute:slocker:0.1.0")` — or, in an Android app, `ca.superroute:slocker-android:0.1.0`.
|
|
224
|
+
|
|
225
|
+
```java
|
|
226
|
+
Client sl = Client.builder().token(token).providerId(4611L).build(); // java.net.http, or HttpURLConnection on Android
|
|
227
|
+
Object page = sl.partnerLockerDeliveries(Params.create().put("status", "delivered").put("per_page", 50));
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```kotlin
|
|
231
|
+
val sl = SLockerAsync(Client.builder().token(token).providerId(4611L).transport(OkHttpTransport()).build())
|
|
232
|
+
val page = sl.api.partnerLockerDeliveries(status = "delivered", perPage = 50) // suspend, on Dispatchers.IO
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Answers are plain JSON values (`Map`, `List`, `String`, `Long`, `Double`, `Boolean`, `null`).
|
|
236
|
+
|
|
237
|
+
### Go
|
|
238
|
+
|
|
239
|
+
```go
|
|
240
|
+
sl := slocker.New(slocker.Config{Token: token, ProviderID: 4611})
|
|
241
|
+
page, err := sl.PartnerLockerDeliveries(ctx, slocker.Params{"status": "delivered", "per_page": 50})
|
|
242
|
+
var e *slocker.Error
|
|
243
|
+
if errors.As(err, &e) && e.Code == "LOCKER_RATE_LIMITED" { /* … */ }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### C#
|
|
247
|
+
|
|
248
|
+
```csharp
|
|
249
|
+
var sl = new SLockerClient(new SLockerOptions { Token = token, ProviderId = 4611 });
|
|
250
|
+
var page = await sl.PartnerLockerDeliveriesAsync(status: "delivered", perPage: 50);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Swift
|
|
254
|
+
|
|
255
|
+
```swift
|
|
256
|
+
let sl = SLockerClient(token: token, providerId: 4611)
|
|
257
|
+
let page = try await sl.partnerLockerDeliveries(status: "delivered", perPage: 50)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Dart and Flutter
|
|
261
|
+
|
|
262
|
+
```dart
|
|
263
|
+
final sl = SLockerClient(token: token, providerId: 4611);
|
|
264
|
+
final page = await sl.partnerLockerDeliveries(status: 'delivered', perPage: 50);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Every client takes its own transport (for tests, proxies or an HTTP stack the app already has), a base URL (white-label
|
|
268
|
+
API hosts and test environments), the retry count and the wait function.
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
specs/ the partner locker slice of Superroute's REST and GraphQL documents, and operations.json
|
|
274
|
+
tools/sync_specs.py copies the slice from a Superroute checkout (and checks no route lacks its Swagger entry)
|
|
275
|
+
tools/generate.py writes every language's operation table and typed methods from the spec
|
|
276
|
+
tools/make_client_cases.py writes tests/cases/client.json: requests, answers, retries, sign-in
|
|
277
|
+
tools/make_webhook_vectors.py writes tests/cases/webhooks.json, signed exactly as Superroute signs
|
|
278
|
+
tests/cases/ the cases every language runs
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```sh
|
|
282
|
+
python3 -I tools/sync_specs.py ../Superroute # then commit specs/ and the generated files
|
|
283
|
+
cd python && python3 -m unittest discover -s tests
|
|
284
|
+
cd js && npm test
|
|
285
|
+
cd php && php tests/run.php
|
|
286
|
+
cd go && go test ./...
|
|
287
|
+
cd java && ./gradlew :slocker:test :slocker-android:test
|
|
288
|
+
dotnet test dotnet/tests/Superroute.SLocker.Tests
|
|
289
|
+
cd swift && swift test
|
|
290
|
+
cd dart && dart pub get && dart test
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Never edit generated files (`generated/`, `Generated/`, `_operations.py`, `operations_gen.go`, `SLockerOperations.kt`):
|
|
294
|
+
change Superroute's spec, sync, and generate again.
|
|
295
|
+
|
|
296
|
+
## Releases
|
|
297
|
+
|
|
298
|
+
sLocker started at 0.1.0; each release adds 0.0.1 to the last one, skipping every version that contains the digit 3
|
|
299
|
+
or 4 (0.1.0, 0.1.1, 0.1.2, 0.1.5, 0.1.6, …, 0.1.9, 0.2.0, …). Tagging a version (`0.1.1`, no `v`) runs
|
|
300
|
+
`.github/workflows/release.yml`, which tests every language and then publishes:
|
|
301
|
+
|
|
302
|
+
- npm (`@superrouteca/slocker`), PyPI (`superroute-slocker`), NuGet (`Superroute.SLocker`) and pub.dev (`slocker`)
|
|
303
|
+
directly, by trusted publishing;
|
|
304
|
+
- to the public repository `Goodneo-Solutions/slocker-releases`: the Swift package (a prebuilt XCFramework attached to
|
|
305
|
+
that repository's GitHub Release of the version), the Maven repository (`maven/`, served by GitHub Pages) with
|
|
306
|
+
`ca.superroute:slocker` and `slocker-android`, and the Go module (`go/`, tag `go/v<version>`);
|
|
307
|
+
- to the public repository `Goodneo-Solutions/slocker-php`: the PHP package at its root, tag `v<version>`, which
|
|
308
|
+
Packagist reads.
|
|
309
|
+
|
|
310
|
+
This source repository stays private; the packages are public under the Superroute SDK License (see [LICENSE](LICENSE)).
|