@deepdockai/cli 1.1.0 → 1.1.2
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.
- package/CHANGELOG.md +8 -0
- package/dist/deepdock.js +2 -1
- package/package.json +1 -1
- package/skill/references/calling-actions.md +6 -2
- package/skill/references/clients/__pycache__/deepdock.cpython-312.pyc +0 -0
- package/skill/references/clients/deepdock.py +36 -4
- package/skill/references/clients/deepdock.ts +53 -3
- package/skill/references/errors.md +1 -0
- package/skill/references/example-integration.md +1 -1
- package/skill/references/idempotency.md +1 -1
- package/skill/references/clients/__pycache__/deepdock.cpython-313.pyc +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.2
|
|
4
|
+
|
|
5
|
+
- The bundled TypeScript and Python clients wait 35 seconds per attempt (was 30): a call can wait up to 10 seconds for the device's slot and then up to 20 seconds on the device. They also retry `503 target_busy` up to three attempts (`maxAttempts` / `max_attempts`), waiting the `Retry-After` seconds (at most 15, one second without the header) and sending the same request, so a write keeps its idempotency key. `DeepdockError` carries the header as `retryAfter` / `retry_after`.
|
|
6
|
+
|
|
7
|
+
## 1.1.1
|
|
8
|
+
|
|
9
|
+
- A new error code, `partner_deactivated` (403): Deepdock has deactivated the organisation, so none of its keys work and its devices are disconnected until Deepdock reactivates it. The CLI exits with 2 on it, as on `partner_not_approved`. The bundled device types and the skill's error reference include it.
|
|
10
|
+
|
|
3
11
|
## 1.1.0
|
|
4
12
|
|
|
5
13
|
- Each system in a device's `systems` answer carries `connectors`: the connectors the device runs for that system, each with a `name` and a `kind` (`database` or `http`). The bundled device types and the client's `DeviceSystem` include it.
|
package/dist/deepdock.js
CHANGED
|
@@ -10852,6 +10852,7 @@ var EXIT_CODES = {
|
|
|
10852
10852
|
invalid_request: 2,
|
|
10853
10853
|
forbidden: 2,
|
|
10854
10854
|
partner_not_approved: 2,
|
|
10855
|
+
partner_deactivated: 2,
|
|
10855
10856
|
not_found: 2,
|
|
10856
10857
|
device_not_found: 2,
|
|
10857
10858
|
system_not_on_device: 2,
|
|
@@ -11440,7 +11441,7 @@ import { spawn } from "node:child_process";
|
|
|
11440
11441
|
import { homedir, hostname } from "node:os";
|
|
11441
11442
|
|
|
11442
11443
|
// src/version.ts
|
|
11443
|
-
var VERSION = "1.1.
|
|
11444
|
+
var VERSION = "1.1.2";
|
|
11444
11445
|
|
|
11445
11446
|
// src/runtime.ts
|
|
11446
11447
|
var DEFAULT_BASE_URL = "https://api.deepdock.ai";
|
package/package.json
CHANGED
|
@@ -114,8 +114,12 @@ Never invent an id, copy one from another device, or hard-code one from a test r
|
|
|
114
114
|
|
|
115
115
|
- Every action call is sent to the customer's PC and run there. Expect a read to take from a few
|
|
116
116
|
hundred milliseconds to a few seconds; a write about the same plus its verification.
|
|
117
|
-
-
|
|
118
|
-
|
|
117
|
+
- A call can wait up to 10 seconds for the device's slot (`503 target_busy` with a `Retry-After`
|
|
118
|
+
header when it stays taken), then gets up to 20 seconds on the device (`503 device_offline`), so
|
|
119
|
+
the worst answer takes about 30 seconds. Set your HTTP client timeout to at least 35 seconds so
|
|
120
|
+
you receive the code instead of timing out first. The bundled clients do this and retry
|
|
121
|
+
`503 target_busy` up to three attempts, waiting the `Retry-After` seconds (at most 15), with the
|
|
122
|
+
same `Idempotency-Key`. A write that
|
|
119
123
|
timed out or failed on the network on your side may still have landed: retry with the same
|
|
120
124
|
`Idempotency-Key`.
|
|
121
125
|
- An answer that is not JSON (a proxy's 502 or 504 page) is a transport failure: for a write, the
|
|
Binary file
|
|
@@ -8,6 +8,7 @@ Everything action-specific (fields, types, patterns) comes from the live OpenAPI
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
+
import time
|
|
11
12
|
from dataclasses import dataclass, field, replace
|
|
12
13
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
13
14
|
from urllib.parse import quote
|
|
@@ -33,9 +34,22 @@ class DeepdockError(Exception):
|
|
|
33
34
|
self.version = version # system_version_unverified: the version the system last reported, None when it reported none
|
|
34
35
|
self.reason = reason # system_version_unverified: unverified_version, unparseable or unreported
|
|
35
36
|
self.created = created # applied_unverified, outcome_unknown with no rollback: {"chartId"} or None
|
|
37
|
+
self.retry_after: Optional[str] = None # the Retry-After header as sent (seconds), when the answer carried one
|
|
36
38
|
self.uncertain = uncertain # outcome_unknown: True when the write may or may not have landed
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
MAX_RETRY_WAIT = 15.0 # longest wait between two attempts, whatever Retry-After asks for
|
|
42
|
+
DEFAULT_RETRY_WAIT = 1.0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _retry_wait(header: Optional[str]) -> float:
|
|
46
|
+
try:
|
|
47
|
+
seconds = float(header) if header is not None else -1.0
|
|
48
|
+
except ValueError:
|
|
49
|
+
seconds = -1.0
|
|
50
|
+
return min(seconds, MAX_RETRY_WAIT) if seconds >= 0 else DEFAULT_RETRY_WAIT
|
|
51
|
+
|
|
52
|
+
|
|
39
53
|
class DeepdockTransportError(Exception):
|
|
40
54
|
"""No answer, or not Deepdock's envelope (timeout, network, a proxy page): a write may still have landed."""
|
|
41
55
|
|
|
@@ -53,15 +67,31 @@ class WriteResult:
|
|
|
53
67
|
|
|
54
68
|
|
|
55
69
|
class Deepdock:
|
|
56
|
-
def __init__(self, api_key: str, base: Optional[str] = None, timeout: float =
|
|
57
|
-
client: Optional[httpx.Client] = None):
|
|
58
|
-
#
|
|
70
|
+
def __init__(self, api_key: str, base: Optional[str] = None, timeout: float = 35.0,
|
|
71
|
+
client: Optional[httpx.Client] = None, max_attempts: int = 3, sleep=time.sleep):
|
|
72
|
+
# 35 s is above the worst answer (up to 10 s for the device's slot, then up to 20 s on the
|
|
73
|
+
# device), so the error code arrives before a client timeout.
|
|
74
|
+
# max_attempts: attempts per call on 503 target_busy (1 turns the retry off). The same
|
|
75
|
+
# request is sent again, so a write keeps its idempotency key.
|
|
76
|
+
self.max_attempts = max(1, int(max_attempts))
|
|
77
|
+
self._sleep = sleep
|
|
59
78
|
# Default: DEEPDOCK_BASE_URL from the environment (the CLI's own variable), else production.
|
|
60
79
|
base = base or os.environ.get("DEEPDOCK_BASE_URL") or "https://api.deepdock.ai"
|
|
61
80
|
self.c = client or httpx.Client(base_url=base.rstrip("/"), timeout=timeout)
|
|
62
81
|
self.c.headers["Authorization"] = f"Bearer {api_key}"
|
|
63
82
|
|
|
64
83
|
def _send(self, method: str, path: str, **kw: Any) -> Tuple[Any, httpx.Headers]:
|
|
84
|
+
attempt = 1
|
|
85
|
+
while True:
|
|
86
|
+
try:
|
|
87
|
+
return self._send_once(method, path, **kw)
|
|
88
|
+
except DeepdockError as e:
|
|
89
|
+
if attempt >= self.max_attempts or e.status != 503 or e.code != "target_busy":
|
|
90
|
+
raise
|
|
91
|
+
self._sleep(_retry_wait(e.retry_after))
|
|
92
|
+
attempt += 1
|
|
93
|
+
|
|
94
|
+
def _send_once(self, method: str, path: str, **kw: Any) -> Tuple[Any, httpx.Headers]:
|
|
65
95
|
try:
|
|
66
96
|
r = self.c.request(method, path, **kw)
|
|
67
97
|
except httpx.HTTPError as e: # timeouts and network errors
|
|
@@ -72,8 +102,10 @@ class Deepdock:
|
|
|
72
102
|
raise DeepdockTransportError(f"non-JSON answer with status {r.status_code}")
|
|
73
103
|
if isinstance(body, dict) and "error" in body:
|
|
74
104
|
e = body["error"]
|
|
75
|
-
|
|
105
|
+
err = DeepdockError(r.status_code, e["code"], e["message"], e.get("requestId"), e.get("systems"),
|
|
76
106
|
e.get("version"), e.get("reason"), e.get("created"), e.get("uncertain"))
|
|
107
|
+
err.retry_after = r.headers.get("retry-after")
|
|
108
|
+
raise err
|
|
77
109
|
if not isinstance(body, dict) or "data" not in body:
|
|
78
110
|
raise DeepdockTransportError(f"unexpected body with status {r.status_code}")
|
|
79
111
|
return body["data"], r.headers
|
|
@@ -21,6 +21,8 @@ export class DeepdockError extends Error {
|
|
|
21
21
|
readonly created?: { chartId?: string } | null
|
|
22
22
|
/** outcome_unknown: `true` when the write may or may not have landed. */
|
|
23
23
|
readonly uncertain?: boolean
|
|
24
|
+
/** The `Retry-After` header as sent (seconds), when the answer carried one. */
|
|
25
|
+
retryAfter?: string
|
|
24
26
|
|
|
25
27
|
constructor(status: number, code: string, message: string, requestId?: string,
|
|
26
28
|
systems?: string[],
|
|
@@ -131,9 +133,24 @@ export type DeviceSystem = {
|
|
|
131
133
|
export type DeepdockOptions = {
|
|
132
134
|
/** Default: `DEEPDOCK_BASE_URL` from the environment (the CLI's own variable), else production. */
|
|
133
135
|
base?: string
|
|
134
|
-
/**
|
|
136
|
+
/** Per attempt, default 35 s: above the worst answer (up to 10 s for the device's slot, then up to 20 s on the device), so you receive the code instead of timing out first. */
|
|
135
137
|
timeoutMs?: number
|
|
138
|
+
/** Attempts per call on `503 target_busy`, default 3; 1 turns the retry off. The same request is sent again, so a write keeps its idempotency key. */
|
|
139
|
+
maxAttempts?: number
|
|
136
140
|
fetch?: typeof fetch
|
|
141
|
+
/** Waits between attempts; replace it in tests. */
|
|
142
|
+
sleep?: (ms: number) => Promise<void>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Longest wait between two attempts, whatever `Retry-After` asks for. */
|
|
146
|
+
const MAX_RETRY_WAIT_MS = 15_000
|
|
147
|
+
const DEFAULT_RETRY_WAIT_MS = 1_000
|
|
148
|
+
|
|
149
|
+
const retryWaitMs = (header: string | null): number => {
|
|
150
|
+
const seconds = header === null ? Number.NaN : Number(header)
|
|
151
|
+
return Number.isFinite(seconds) && seconds >= 0
|
|
152
|
+
? Math.min(seconds * 1000, MAX_RETRY_WAIT_MS)
|
|
153
|
+
: DEFAULT_RETRY_WAIT_MS
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
// `process` is absent in a browser, and Deno throws on `process.env` without --allow-env; then the
|
|
@@ -150,21 +167,52 @@ export class Deepdock {
|
|
|
150
167
|
private readonly apiKey: string
|
|
151
168
|
private readonly base: string
|
|
152
169
|
private readonly timeoutMs: number
|
|
170
|
+
private readonly maxAttempts: number
|
|
153
171
|
private readonly fetchImpl: typeof fetch
|
|
172
|
+
private readonly sleep: (ms: number) => Promise<void>
|
|
154
173
|
|
|
155
174
|
constructor(
|
|
156
175
|
apiKey: string,
|
|
157
|
-
{
|
|
176
|
+
{
|
|
177
|
+
base = envBase() ?? 'https://api.deepdock.ai',
|
|
178
|
+
timeoutMs = 35_000,
|
|
179
|
+
maxAttempts = 3,
|
|
180
|
+
fetch: fetchImpl = fetch,
|
|
181
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
182
|
+
}: DeepdockOptions = {},
|
|
158
183
|
) {
|
|
159
184
|
this.apiKey = apiKey
|
|
160
185
|
this.base = base.replace(/\/$/, '')
|
|
161
186
|
this.timeoutMs = timeoutMs
|
|
187
|
+
this.maxAttempts = Math.max(1, Math.floor(maxAttempts))
|
|
162
188
|
this.fetchImpl = fetchImpl
|
|
189
|
+
this.sleep = sleep
|
|
163
190
|
}
|
|
164
191
|
|
|
165
192
|
private async send<T>(
|
|
166
193
|
path: string,
|
|
167
194
|
init: { method: string; headers?: Record<string, string>; body?: string },
|
|
195
|
+
): Promise<{ data: T; headers: Headers }> {
|
|
196
|
+
for (let attempt = 1; ; attempt++) {
|
|
197
|
+
try {
|
|
198
|
+
return await this.sendOnce<T>(path, init)
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (
|
|
201
|
+
attempt >= this.maxAttempts ||
|
|
202
|
+
!(err instanceof DeepdockError) ||
|
|
203
|
+
err.status !== 503 ||
|
|
204
|
+
err.code !== 'target_busy'
|
|
205
|
+
) {
|
|
206
|
+
throw err
|
|
207
|
+
}
|
|
208
|
+
await this.sleep(retryWaitMs(err.retryAfter ?? null))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async sendOnce<T>(
|
|
214
|
+
path: string,
|
|
215
|
+
init: { method: string; headers?: Record<string, string>; body?: string },
|
|
168
216
|
): Promise<{ data: T; headers: Headers }> {
|
|
169
217
|
let res: Response
|
|
170
218
|
try {
|
|
@@ -186,7 +234,9 @@ export class Deepdock {
|
|
|
186
234
|
}
|
|
187
235
|
if (body !== null && typeof body === 'object' && 'error' in body) {
|
|
188
236
|
const { code, message, requestId, systems, version, reason, created, uncertain } = body.error
|
|
189
|
-
|
|
237
|
+
const error = new DeepdockError(res.status, code, message, requestId, systems, version, reason, created, uncertain)
|
|
238
|
+
error.retryAfter = res.headers.get('retry-after') ?? undefined
|
|
239
|
+
throw error
|
|
190
240
|
}
|
|
191
241
|
if (body === null || typeof body !== 'object' || !('data' in body)) {
|
|
192
242
|
throw new DeepdockTransportError(`unexpected body with status ${res.status}`)
|
|
@@ -21,6 +21,7 @@ with the **same** `Idempotency-Key`); **Show** says what the end user should see
|
|
|
21
21
|
| `payload_too_large` | 413 | the request body is over 1 MiB | no; send less | nothing; a developer bug, or shorten the text |
|
|
22
22
|
| `unauthenticated` | 401 | the key is missing, malformed, for the other environment, unknown, revoked or expired | no | "Deepdock API key refused; check it in the dashboard" to an operator |
|
|
23
23
|
| `partner_not_approved` | 403 | your organisation is waiting for approval by Deepdock; no key of it works yet. Keys made before the approval start working once it is given | no, until Deepdock approves; then yes | "our Deepdock account is waiting for approval; contact Deepdock" |
|
|
24
|
+
| `partner_deactivated` | 403 | Deepdock has deactivated your organisation: no key of it works and its devices are disconnected. Keys and devices work again once Deepdock reactivates it | no, until Deepdock reactivates it; then yes | "our Deepdock account is deactivated; contact Deepdock" |
|
|
24
25
|
| `not_found` | 404 | no such route; an update whose id matches no record; a write naming a record that does not exist or belongs to another one | no | "record not found" when it is a record; otherwise a bug |
|
|
25
26
|
| `rate_limited` | 429 | more than 600 calls, or more than 120 writes (counted in both), with this key in a minute; or more than 5 failed pairing claims in 60 s | yes, after `Retry-After: 60` (`retryAfterSeconds: 60` in the body); a write keeps its key. The pairing claim answer has neither: wait up to a minute | "try again in a minute" |
|
|
26
27
|
|
|
@@ -20,7 +20,7 @@ Copy the file into the project; do not reach into the skill directory at runtime
|
|
|
20
20
|
```ts
|
|
21
21
|
import { Deepdock, DeepdockError, DeepdockTransportError, attempt, type PendingWrite } from './deepdock'
|
|
22
22
|
|
|
23
|
-
const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!) // base from DEEPDOCK_BASE_URL, else production;
|
|
23
|
+
const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!) // base from DEEPDOCK_BASE_URL, else production; 35 s timeout, retries 503 target_busy
|
|
24
24
|
const page = await dd.read<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' }, { limit: 50 })
|
|
25
25
|
for await (const p of dd.readAll<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' })) { /* every page */ }
|
|
26
26
|
const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote', 'create', key, input)
|
|
@@ -92,7 +92,7 @@ const res = await fetch(url, {
|
|
|
92
92
|
'Content-Type': 'application/json',
|
|
93
93
|
},
|
|
94
94
|
body: JSON.stringify(input),
|
|
95
|
-
signal: AbortSignal.timeout(
|
|
95
|
+
signal: AbortSignal.timeout(35_000),
|
|
96
96
|
})
|
|
97
97
|
const replayed = res.headers.get('Idempotent-Replayed') === 'true'
|
|
98
98
|
```
|
|
Binary file
|