rightnow-system-zero 0.3.0__tar.gz → 0.4.2__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.
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/NOTICE +1 -1
- {rightnow_system_zero-0.3.0/src/rightnow_system_zero.egg-info → rightnow_system_zero-0.4.2}/PKG-INFO +46 -10
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/README.md +44 -8
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/pyproject.toml +2 -2
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2/src/rightnow_system_zero.egg-info}/PKG-INFO +46 -10
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/SOURCES.txt +2 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/__init__.py +28 -3
- rightnow_system_zero-0.4.2/src/system_zero/_redaction.py +207 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/_retry.py +1 -1
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/_transport.py +2 -2
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/batch.py +8 -6
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/client.py +39 -22
- rightnow_system_zero-0.4.2/src/system_zero/enabled_types.py +227 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/error-catalog.json +118 -110
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/error_codes.py +5 -1
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/errors.py +25 -3
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/evaluate.py +1 -1
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/flows.py +12 -8
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/media.py +23 -5
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/stream.py +190 -24
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/tool-schemas.json +4 -4
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/tools.py +11 -5
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/types.py +11 -3
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/workflow_types.py +4 -1
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/LICENSE +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/setup.cfg +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/dependency_links.txt +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/requires.txt +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/top_level.txt +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/_connection.py +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/argument-limits.json +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/decisions.py +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/public_error_codes.py +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/py.typed +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/response.py +0 -0
- {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/stream_interfaces.py +0 -0
{rightnow_system_zero-0.3.0/src/rightnow_system_zero.egg-info → rightnow_system_zero-0.4.2}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rightnow-system-zero
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: System Zero API client with typed decisions and client-side workflows.
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: RightNow System Zero API client with typed decisions and client-side workflows.
|
|
5
5
|
Author: RightNow AI, Inc.
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Changelog, https://github.com/RightNow-AI/system-zero-sdk/blob/main/CHANGELOG.md
|
|
@@ -29,9 +29,11 @@ Requires-Dist: httpcore<2,>=1.0.9
|
|
|
29
29
|
Requires-Dist: websockets<16,>=15.0.1
|
|
30
30
|
Dynamic: license-file
|
|
31
31
|
|
|
32
|
-
# System Zero Python SDK
|
|
32
|
+
# RightNow System Zero Python SDK
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
RightNow is the model name; System Zero is its category. The full name is RightNow System Zero.
|
|
35
|
+
|
|
36
|
+
Synchronous server-side Python client for the RightNow System Zero API.
|
|
35
37
|
|
|
36
38
|
Requires Python 3.10 or later.
|
|
37
39
|
|
|
@@ -73,9 +75,10 @@ manager to release the HTTP transport.
|
|
|
73
75
|
## Media on decide
|
|
74
76
|
|
|
75
77
|
`attachments` is optional; `null` remains valid. Each inline attachment has a unique
|
|
76
|
-
`name`, a `type` of `image` or `
|
|
78
|
+
`name`, a `type` of `image`, `audio`, `document` or `video`, and a canonical base64 data URL in `data`.
|
|
77
79
|
Names start with an ASCII letter and contain at most 64 letters, digits, `_` or `-`.
|
|
78
|
-
Function tools and the MCP adapter carry the
|
|
80
|
+
Function tools and the MCP adapter carry the image and audio forms.
|
|
81
|
+
Send PDF and video through the ordinary client request form shown below.
|
|
79
82
|
|
|
80
83
|
- Images: static PNG (`image/png`) and JPEG (`image/jpeg`), at most 4 images,
|
|
81
84
|
16,777,216 pixels each, with an aspect ratio no greater than 50:1.
|
|
@@ -96,13 +99,23 @@ prove a complete image is valid.
|
|
|
96
99
|
GIF (`image/gif`), WebP (`image/webp`), animated PNG and images outside the decoded
|
|
97
100
|
profile are refused with `media_unpriced`. A mismatched image signature uses
|
|
98
101
|
`media_decode_failed`; other unsupported image MIME types use `media_unsupported`.
|
|
99
|
-
|
|
100
|
-
`video_not_available`. Other audio formats, rates, malformed WAV and audio upload
|
|
102
|
+
Other audio formats, rates, malformed WAV and audio upload
|
|
101
103
|
references use `media_unsupported`. Image upload references are unavailable and
|
|
102
104
|
return `service_unavailable` in the listed service. Send bytes inline.
|
|
103
105
|
|
|
106
|
+
PDF is supported through ordinary inline attachments.
|
|
107
|
+
Text PDFs and readable scans are read as text.
|
|
108
|
+
Only PDF is supported; other document formats are unavailable.
|
|
109
|
+
Use `type: "document"` and a canonical `data:application/pdf;base64,...` URL.
|
|
110
|
+
The image and audio helpers do not encode PDF; construct its inline attachment directly.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
Video is supported through ordinary inline attachments. Send one silent H.264 MP4 video as an inline base64 data URL. Maximum duration: 10 seconds. Maximum dimensions: 1920 x 1080. Maximum decoded file size: 2,621,440 bytes (2.5 MiB). The complete JSON request must fit 3,665,920 bytes, including base64 and questions. Rotated videos, soundtracks, separate attachments and video upload references are not supported.
|
|
114
|
+
|
|
115
|
+
Use `videoAttachment({ name: 'clip', data: bytes })` in TypeScript or `video_attachment(name='clip', data=bytes)` in Python. Video helpers encode the bytes; the API performs complete-file validation. A `video_not_available` response becomes `VideoNotAvailableError`, preserving status, code, message and request ID.
|
|
104
116
|
```python
|
|
105
117
|
from pathlib import Path
|
|
118
|
+
from base64 import b64encode
|
|
106
119
|
from system_zero import SystemZero, inline_attachment, validate_attachments
|
|
107
120
|
|
|
108
121
|
attachments = validate_attachments([
|
|
@@ -115,6 +128,13 @@ with SystemZero() as client:
|
|
|
115
128
|
"questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
|
|
116
129
|
})
|
|
117
130
|
print(reply["answers"])
|
|
131
|
+
# PDF uses the ordinary inline request form.
|
|
132
|
+
client.decide({
|
|
133
|
+
"model": "system-zero", "state": "Read the PDF.",
|
|
134
|
+
"questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
|
|
135
|
+
"attachments": [{"name": "document", "type": "document",
|
|
136
|
+
"data": "data:application/pdf;base64," + b64encode(Path("document.pdf").read_bytes()).decode("ascii")}],
|
|
137
|
+
})
|
|
118
138
|
```
|
|
119
139
|
|
|
120
140
|
`MIMES`, `MEDIA_LIMITS` and `attachment_magic_matches` expose the served registry,
|
|
@@ -133,7 +153,7 @@ attempts are limited by the remaining budget. A wait above the maximum wait, or
|
|
|
133
153
|
one that exhausts the remaining budget, raises the last error without waiting.
|
|
134
154
|
A retry needs at least one millisecond remaining.
|
|
135
155
|
|
|
136
|
-
Only GET requests or requests carrying a
|
|
156
|
+
Only GET requests or requests carrying a non-empty idempotency key can retry.
|
|
137
157
|
With retries enabled, decide POSTs receive a generated key when none is supplied.
|
|
138
158
|
An explicit blank key omits the header and disables decision retries. Each retry
|
|
139
159
|
retains the same key and serialized body. Setting the retry count or budget to
|
|
@@ -217,7 +237,7 @@ Every HTTP attempt sends `X-SDK-Retry-Count`, starting at `0`, and a
|
|
|
217
237
|
A blank or whitespace-only `client_request_id` omits the
|
|
218
238
|
`X-Client-Request-Id` header.
|
|
219
239
|
|
|
220
|
-
Keep API keys on trusted servers. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
|
|
240
|
+
Keep API keys on trusted servers. Don't enable DEBUG logging for the HTTP or WebSocket libraries in production; it can record request headers, including your API key. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
|
|
221
241
|
for private vulnerability reporting.
|
|
222
242
|
|
|
223
243
|
## Function tool formats
|
|
@@ -299,3 +319,19 @@ Billing is per decision, not per connection, heartbeat, acknowledgement, or
|
|
|
299
319
|
mutation. Several mutations can be covered by one decision. A decision may be
|
|
300
320
|
counted even if the connection closes before its event arrives; use the event's
|
|
301
321
|
usage and request identifier when reconciling completed work.
|
|
322
|
+
|
|
323
|
+
## LongContextRequest opt-in
|
|
324
|
+
|
|
325
|
+
The SDK publishes the `LongContextRequest` type as an opt-in surface. It is unavailable until the API enables it for the workspace; workspace opt-in through the console's Settings is also required. Once available, give explicit per-request consent with `dangerZone: 'accept'` in TypeScript or `danger_zone="accept"` in Python. The option sends exactly `SZ-Danger-Zone: accept`; it is never sent by default or inferred from state size. Until availability, the API ignores or refuses this option. Do not supply this header through custom headers. Retries retain the option. The same explicit option is supported by batch and flow helpers.
|
|
326
|
+
|
|
327
|
+
Until workspace enablement, the expected result is HTTP 404 `long_context_not_available`, surfaced as `LongContextNotAvailableError`. The same refusal applies without workspace opt-in. When the API supplies `details.eligibility`, `LongContextNotAvailableError.eligibility` exposes the rule: `billing`, `opt_in`, `class`, `deny_list`, `global_switch` or `account_deleted`, with a string fallback for future rules. A generic held-route refusal can omit the rule. Status, code, message and request ID remain intact. Held routes are never retried.
|
|
328
|
+
|
|
329
|
+
`LongContextRequest`, `EnabledDecideRequest`, `OrdinaryRequest`, `MediaRequest` and their question types describe the separate enabled request contract in `enabled.openapi.json`. `LongContextLimits` describes its discovery limits. `openapi.json` remains the served contract. The API selects the request band and checks bounds: a `LongContextRequest` takes at most 32 questions, choices with 2 to 25 options, and scores with 2 to 4 levels. Only inline document attachments fit its document branch. The API validates document admission separately.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
await client.decide(request, { dangerZone: 'accept' });
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
client.decide(request, danger_zone="accept")
|
|
337
|
+
```
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# System Zero Python SDK
|
|
1
|
+
# RightNow System Zero Python SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
RightNow is the model name; System Zero is its category. The full name is RightNow System Zero.
|
|
4
|
+
|
|
5
|
+
Synchronous server-side Python client for the RightNow System Zero API.
|
|
4
6
|
|
|
5
7
|
Requires Python 3.10 or later.
|
|
6
8
|
|
|
@@ -42,9 +44,10 @@ manager to release the HTTP transport.
|
|
|
42
44
|
## Media on decide
|
|
43
45
|
|
|
44
46
|
`attachments` is optional; `null` remains valid. Each inline attachment has a unique
|
|
45
|
-
`name`, a `type` of `image` or `
|
|
47
|
+
`name`, a `type` of `image`, `audio`, `document` or `video`, and a canonical base64 data URL in `data`.
|
|
46
48
|
Names start with an ASCII letter and contain at most 64 letters, digits, `_` or `-`.
|
|
47
|
-
Function tools and the MCP adapter carry the
|
|
49
|
+
Function tools and the MCP adapter carry the image and audio forms.
|
|
50
|
+
Send PDF and video through the ordinary client request form shown below.
|
|
48
51
|
|
|
49
52
|
- Images: static PNG (`image/png`) and JPEG (`image/jpeg`), at most 4 images,
|
|
50
53
|
16,777,216 pixels each, with an aspect ratio no greater than 50:1.
|
|
@@ -65,13 +68,23 @@ prove a complete image is valid.
|
|
|
65
68
|
GIF (`image/gif`), WebP (`image/webp`), animated PNG and images outside the decoded
|
|
66
69
|
profile are refused with `media_unpriced`. A mismatched image signature uses
|
|
67
70
|
`media_decode_failed`; other unsupported image MIME types use `media_unsupported`.
|
|
68
|
-
|
|
69
|
-
`video_not_available`. Other audio formats, rates, malformed WAV and audio upload
|
|
71
|
+
Other audio formats, rates, malformed WAV and audio upload
|
|
70
72
|
references use `media_unsupported`. Image upload references are unavailable and
|
|
71
73
|
return `service_unavailable` in the listed service. Send bytes inline.
|
|
72
74
|
|
|
75
|
+
PDF is supported through ordinary inline attachments.
|
|
76
|
+
Text PDFs and readable scans are read as text.
|
|
77
|
+
Only PDF is supported; other document formats are unavailable.
|
|
78
|
+
Use `type: "document"` and a canonical `data:application/pdf;base64,...` URL.
|
|
79
|
+
The image and audio helpers do not encode PDF; construct its inline attachment directly.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Video is supported through ordinary inline attachments. Send one silent H.264 MP4 video as an inline base64 data URL. Maximum duration: 10 seconds. Maximum dimensions: 1920 x 1080. Maximum decoded file size: 2,621,440 bytes (2.5 MiB). The complete JSON request must fit 3,665,920 bytes, including base64 and questions. Rotated videos, soundtracks, separate attachments and video upload references are not supported.
|
|
83
|
+
|
|
84
|
+
Use `videoAttachment({ name: 'clip', data: bytes })` in TypeScript or `video_attachment(name='clip', data=bytes)` in Python. Video helpers encode the bytes; the API performs complete-file validation. A `video_not_available` response becomes `VideoNotAvailableError`, preserving status, code, message and request ID.
|
|
73
85
|
```python
|
|
74
86
|
from pathlib import Path
|
|
87
|
+
from base64 import b64encode
|
|
75
88
|
from system_zero import SystemZero, inline_attachment, validate_attachments
|
|
76
89
|
|
|
77
90
|
attachments = validate_attachments([
|
|
@@ -84,6 +97,13 @@ with SystemZero() as client:
|
|
|
84
97
|
"questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
|
|
85
98
|
})
|
|
86
99
|
print(reply["answers"])
|
|
100
|
+
# PDF uses the ordinary inline request form.
|
|
101
|
+
client.decide({
|
|
102
|
+
"model": "system-zero", "state": "Read the PDF.",
|
|
103
|
+
"questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
|
|
104
|
+
"attachments": [{"name": "document", "type": "document",
|
|
105
|
+
"data": "data:application/pdf;base64," + b64encode(Path("document.pdf").read_bytes()).decode("ascii")}],
|
|
106
|
+
})
|
|
87
107
|
```
|
|
88
108
|
|
|
89
109
|
`MIMES`, `MEDIA_LIMITS` and `attachment_magic_matches` expose the served registry,
|
|
@@ -102,7 +122,7 @@ attempts are limited by the remaining budget. A wait above the maximum wait, or
|
|
|
102
122
|
one that exhausts the remaining budget, raises the last error without waiting.
|
|
103
123
|
A retry needs at least one millisecond remaining.
|
|
104
124
|
|
|
105
|
-
Only GET requests or requests carrying a
|
|
125
|
+
Only GET requests or requests carrying a non-empty idempotency key can retry.
|
|
106
126
|
With retries enabled, decide POSTs receive a generated key when none is supplied.
|
|
107
127
|
An explicit blank key omits the header and disables decision retries. Each retry
|
|
108
128
|
retains the same key and serialized body. Setting the retry count or budget to
|
|
@@ -186,7 +206,7 @@ Every HTTP attempt sends `X-SDK-Retry-Count`, starting at `0`, and a
|
|
|
186
206
|
A blank or whitespace-only `client_request_id` omits the
|
|
187
207
|
`X-Client-Request-Id` header.
|
|
188
208
|
|
|
189
|
-
Keep API keys on trusted servers. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
|
|
209
|
+
Keep API keys on trusted servers. Don't enable DEBUG logging for the HTTP or WebSocket libraries in production; it can record request headers, including your API key. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
|
|
190
210
|
for private vulnerability reporting.
|
|
191
211
|
|
|
192
212
|
## Function tool formats
|
|
@@ -268,3 +288,19 @@ Billing is per decision, not per connection, heartbeat, acknowledgement, or
|
|
|
268
288
|
mutation. Several mutations can be covered by one decision. A decision may be
|
|
269
289
|
counted even if the connection closes before its event arrives; use the event's
|
|
270
290
|
usage and request identifier when reconciling completed work.
|
|
291
|
+
|
|
292
|
+
## LongContextRequest opt-in
|
|
293
|
+
|
|
294
|
+
The SDK publishes the `LongContextRequest` type as an opt-in surface. It is unavailable until the API enables it for the workspace; workspace opt-in through the console's Settings is also required. Once available, give explicit per-request consent with `dangerZone: 'accept'` in TypeScript or `danger_zone="accept"` in Python. The option sends exactly `SZ-Danger-Zone: accept`; it is never sent by default or inferred from state size. Until availability, the API ignores or refuses this option. Do not supply this header through custom headers. Retries retain the option. The same explicit option is supported by batch and flow helpers.
|
|
295
|
+
|
|
296
|
+
Until workspace enablement, the expected result is HTTP 404 `long_context_not_available`, surfaced as `LongContextNotAvailableError`. The same refusal applies without workspace opt-in. When the API supplies `details.eligibility`, `LongContextNotAvailableError.eligibility` exposes the rule: `billing`, `opt_in`, `class`, `deny_list`, `global_switch` or `account_deleted`, with a string fallback for future rules. A generic held-route refusal can omit the rule. Status, code, message and request ID remain intact. Held routes are never retried.
|
|
297
|
+
|
|
298
|
+
`LongContextRequest`, `EnabledDecideRequest`, `OrdinaryRequest`, `MediaRequest` and their question types describe the separate enabled request contract in `enabled.openapi.json`. `LongContextLimits` describes its discovery limits. `openapi.json` remains the served contract. The API selects the request band and checks bounds: a `LongContextRequest` takes at most 32 questions, choices with 2 to 25 options, and scores with 2 to 4 levels. Only inline document attachments fit its document branch. The API validates document admission separately.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
await client.decide(request, { dangerZone: 'accept' });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
client.decide(request, danger_zone="accept")
|
|
306
|
+
```
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rightnow-system-zero"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "System Zero API client with typed decisions and client-side workflows."
|
|
7
|
+
version = "0.4.2"
|
|
8
|
+
description = "RightNow System Zero API client with typed decisions and client-side workflows."
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
license-files = ["LICENSE", "NOTICE"]
|
|
11
11
|
readme = "README.md"
|
{rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2/src/rightnow_system_zero.egg-info}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rightnow-system-zero
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: System Zero API client with typed decisions and client-side workflows.
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: RightNow System Zero API client with typed decisions and client-side workflows.
|
|
5
5
|
Author: RightNow AI, Inc.
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Changelog, https://github.com/RightNow-AI/system-zero-sdk/blob/main/CHANGELOG.md
|
|
@@ -29,9 +29,11 @@ Requires-Dist: httpcore<2,>=1.0.9
|
|
|
29
29
|
Requires-Dist: websockets<16,>=15.0.1
|
|
30
30
|
Dynamic: license-file
|
|
31
31
|
|
|
32
|
-
# System Zero Python SDK
|
|
32
|
+
# RightNow System Zero Python SDK
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
RightNow is the model name; System Zero is its category. The full name is RightNow System Zero.
|
|
35
|
+
|
|
36
|
+
Synchronous server-side Python client for the RightNow System Zero API.
|
|
35
37
|
|
|
36
38
|
Requires Python 3.10 or later.
|
|
37
39
|
|
|
@@ -73,9 +75,10 @@ manager to release the HTTP transport.
|
|
|
73
75
|
## Media on decide
|
|
74
76
|
|
|
75
77
|
`attachments` is optional; `null` remains valid. Each inline attachment has a unique
|
|
76
|
-
`name`, a `type` of `image` or `
|
|
78
|
+
`name`, a `type` of `image`, `audio`, `document` or `video`, and a canonical base64 data URL in `data`.
|
|
77
79
|
Names start with an ASCII letter and contain at most 64 letters, digits, `_` or `-`.
|
|
78
|
-
Function tools and the MCP adapter carry the
|
|
80
|
+
Function tools and the MCP adapter carry the image and audio forms.
|
|
81
|
+
Send PDF and video through the ordinary client request form shown below.
|
|
79
82
|
|
|
80
83
|
- Images: static PNG (`image/png`) and JPEG (`image/jpeg`), at most 4 images,
|
|
81
84
|
16,777,216 pixels each, with an aspect ratio no greater than 50:1.
|
|
@@ -96,13 +99,23 @@ prove a complete image is valid.
|
|
|
96
99
|
GIF (`image/gif`), WebP (`image/webp`), animated PNG and images outside the decoded
|
|
97
100
|
profile are refused with `media_unpriced`. A mismatched image signature uses
|
|
98
101
|
`media_decode_failed`; other unsupported image MIME types use `media_unsupported`.
|
|
99
|
-
|
|
100
|
-
`video_not_available`. Other audio formats, rates, malformed WAV and audio upload
|
|
102
|
+
Other audio formats, rates, malformed WAV and audio upload
|
|
101
103
|
references use `media_unsupported`. Image upload references are unavailable and
|
|
102
104
|
return `service_unavailable` in the listed service. Send bytes inline.
|
|
103
105
|
|
|
106
|
+
PDF is supported through ordinary inline attachments.
|
|
107
|
+
Text PDFs and readable scans are read as text.
|
|
108
|
+
Only PDF is supported; other document formats are unavailable.
|
|
109
|
+
Use `type: "document"` and a canonical `data:application/pdf;base64,...` URL.
|
|
110
|
+
The image and audio helpers do not encode PDF; construct its inline attachment directly.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
Video is supported through ordinary inline attachments. Send one silent H.264 MP4 video as an inline base64 data URL. Maximum duration: 10 seconds. Maximum dimensions: 1920 x 1080. Maximum decoded file size: 2,621,440 bytes (2.5 MiB). The complete JSON request must fit 3,665,920 bytes, including base64 and questions. Rotated videos, soundtracks, separate attachments and video upload references are not supported.
|
|
114
|
+
|
|
115
|
+
Use `videoAttachment({ name: 'clip', data: bytes })` in TypeScript or `video_attachment(name='clip', data=bytes)` in Python. Video helpers encode the bytes; the API performs complete-file validation. A `video_not_available` response becomes `VideoNotAvailableError`, preserving status, code, message and request ID.
|
|
104
116
|
```python
|
|
105
117
|
from pathlib import Path
|
|
118
|
+
from base64 import b64encode
|
|
106
119
|
from system_zero import SystemZero, inline_attachment, validate_attachments
|
|
107
120
|
|
|
108
121
|
attachments = validate_attachments([
|
|
@@ -115,6 +128,13 @@ with SystemZero() as client:
|
|
|
115
128
|
"questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
|
|
116
129
|
})
|
|
117
130
|
print(reply["answers"])
|
|
131
|
+
# PDF uses the ordinary inline request form.
|
|
132
|
+
client.decide({
|
|
133
|
+
"model": "system-zero", "state": "Read the PDF.",
|
|
134
|
+
"questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
|
|
135
|
+
"attachments": [{"name": "document", "type": "document",
|
|
136
|
+
"data": "data:application/pdf;base64," + b64encode(Path("document.pdf").read_bytes()).decode("ascii")}],
|
|
137
|
+
})
|
|
118
138
|
```
|
|
119
139
|
|
|
120
140
|
`MIMES`, `MEDIA_LIMITS` and `attachment_magic_matches` expose the served registry,
|
|
@@ -133,7 +153,7 @@ attempts are limited by the remaining budget. A wait above the maximum wait, or
|
|
|
133
153
|
one that exhausts the remaining budget, raises the last error without waiting.
|
|
134
154
|
A retry needs at least one millisecond remaining.
|
|
135
155
|
|
|
136
|
-
Only GET requests or requests carrying a
|
|
156
|
+
Only GET requests or requests carrying a non-empty idempotency key can retry.
|
|
137
157
|
With retries enabled, decide POSTs receive a generated key when none is supplied.
|
|
138
158
|
An explicit blank key omits the header and disables decision retries. Each retry
|
|
139
159
|
retains the same key and serialized body. Setting the retry count or budget to
|
|
@@ -217,7 +237,7 @@ Every HTTP attempt sends `X-SDK-Retry-Count`, starting at `0`, and a
|
|
|
217
237
|
A blank or whitespace-only `client_request_id` omits the
|
|
218
238
|
`X-Client-Request-Id` header.
|
|
219
239
|
|
|
220
|
-
Keep API keys on trusted servers. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
|
|
240
|
+
Keep API keys on trusted servers. Don't enable DEBUG logging for the HTTP or WebSocket libraries in production; it can record request headers, including your API key. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
|
|
221
241
|
for private vulnerability reporting.
|
|
222
242
|
|
|
223
243
|
## Function tool formats
|
|
@@ -299,3 +319,19 @@ Billing is per decision, not per connection, heartbeat, acknowledgement, or
|
|
|
299
319
|
mutation. Several mutations can be covered by one decision. A decision may be
|
|
300
320
|
counted even if the connection closes before its event arrives; use the event's
|
|
301
321
|
usage and request identifier when reconciling completed work.
|
|
322
|
+
|
|
323
|
+
## LongContextRequest opt-in
|
|
324
|
+
|
|
325
|
+
The SDK publishes the `LongContextRequest` type as an opt-in surface. It is unavailable until the API enables it for the workspace; workspace opt-in through the console's Settings is also required. Once available, give explicit per-request consent with `dangerZone: 'accept'` in TypeScript or `danger_zone="accept"` in Python. The option sends exactly `SZ-Danger-Zone: accept`; it is never sent by default or inferred from state size. Until availability, the API ignores or refuses this option. Do not supply this header through custom headers. Retries retain the option. The same explicit option is supported by batch and flow helpers.
|
|
326
|
+
|
|
327
|
+
Until workspace enablement, the expected result is HTTP 404 `long_context_not_available`, surfaced as `LongContextNotAvailableError`. The same refusal applies without workspace opt-in. When the API supplies `details.eligibility`, `LongContextNotAvailableError.eligibility` exposes the rule: `billing`, `opt_in`, `class`, `deny_list`, `global_switch` or `account_deleted`, with a string fallback for future rules. A generic held-route refusal can omit the rule. Status, code, message and request ID remain intact. Held routes are never retried.
|
|
328
|
+
|
|
329
|
+
`LongContextRequest`, `EnabledDecideRequest`, `OrdinaryRequest`, `MediaRequest` and their question types describe the separate enabled request contract in `enabled.openapi.json`. `LongContextLimits` describes its discovery limits. `openapi.json` remains the served contract. The API selects the request band and checks bounds: a `LongContextRequest` takes at most 32 questions, choices with 2 to 25 options, and scores with 2 to 4 levels. Only inline document attachments fit its document branch. The API validates document admission separately.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
await client.decide(request, { dangerZone: 'accept' });
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
client.decide(request, danger_zone="accept")
|
|
337
|
+
```
|
|
@@ -9,12 +9,14 @@ src/rightnow_system_zero.egg-info/requires.txt
|
|
|
9
9
|
src/rightnow_system_zero.egg-info/top_level.txt
|
|
10
10
|
src/system_zero/__init__.py
|
|
11
11
|
src/system_zero/_connection.py
|
|
12
|
+
src/system_zero/_redaction.py
|
|
12
13
|
src/system_zero/_retry.py
|
|
13
14
|
src/system_zero/_transport.py
|
|
14
15
|
src/system_zero/argument-limits.json
|
|
15
16
|
src/system_zero/batch.py
|
|
16
17
|
src/system_zero/client.py
|
|
17
18
|
src/system_zero/decisions.py
|
|
19
|
+
src/system_zero/enabled_types.py
|
|
18
20
|
src/system_zero/error-catalog.json
|
|
19
21
|
src/system_zero/error_codes.py
|
|
20
22
|
src/system_zero/errors.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""System Zero API client and client-side helpers."""
|
|
1
|
+
"""RightNow System Zero API client and client-side helpers."""
|
|
2
2
|
|
|
3
3
|
from .client import (
|
|
4
4
|
VERSION,
|
|
@@ -143,6 +143,19 @@ from .workflow_types import (
|
|
|
143
143
|
Usage,
|
|
144
144
|
)
|
|
145
145
|
|
|
146
|
+
from .enabled_types import (
|
|
147
|
+
EnabledDecideRequest,
|
|
148
|
+
LongContextLimits,
|
|
149
|
+
LongContextQuestion,
|
|
150
|
+
LongContextRequest,
|
|
151
|
+
MediaQuestion,
|
|
152
|
+
MediaRequest,
|
|
153
|
+
OrdinaryQuestion,
|
|
154
|
+
OrdinaryRequest,
|
|
155
|
+
VideoAttachment,
|
|
156
|
+
VideoRequest,
|
|
157
|
+
)
|
|
158
|
+
|
|
146
159
|
from .tools import (
|
|
147
160
|
NESTED_FUNCTION_TOOLS,
|
|
148
161
|
FLAT_FUNCTION_TOOLS,
|
|
@@ -155,7 +168,7 @@ from .tools import (
|
|
|
155
168
|
__version__ = VERSION
|
|
156
169
|
|
|
157
170
|
from .types import Attachment
|
|
158
|
-
from .media import (ByteData, MAX_ATTACHMENTS, MAX_INLINE_BYTES, MEDIA_LIMITS, MIMES, attachment_magic_matches, inline_attachment, validate_attachments)
|
|
171
|
+
from .media import (ByteData, MAX_ATTACHMENTS, MAX_INLINE_BYTES, MEDIA_LIMITS, MIMES, VIDEO_PROFILE, attachment_magic_matches, inline_attachment, validate_attachments, video_attachment)
|
|
159
172
|
|
|
160
173
|
__all__ = [
|
|
161
174
|
"APIConnectionError",
|
|
@@ -180,6 +193,7 @@ __all__ = [
|
|
|
180
193
|
"DecisionMode",
|
|
181
194
|
"DeliberateNotAvailableError",
|
|
182
195
|
"DocumentNotAvailableError",
|
|
196
|
+
"EnabledDecideRequest",
|
|
183
197
|
"ErrorCode",
|
|
184
198
|
"EvaluationRecord",
|
|
185
199
|
"EvaluationReport",
|
|
@@ -206,17 +220,24 @@ __all__ = [
|
|
|
206
220
|
"InvalidRequestError",
|
|
207
221
|
"KNOWN_ERROR_CODES",
|
|
208
222
|
"LegacyDecision",
|
|
223
|
+
"LongContextLimits",
|
|
209
224
|
"LongContextNotAvailableError",
|
|
225
|
+
"LongContextQuestion",
|
|
226
|
+
"LongContextRequest",
|
|
210
227
|
"LongRecordingNotAvailableError",
|
|
211
228
|
"MAX_ATTACHMENTS",
|
|
212
229
|
"MAX_FLOW_STEPS",
|
|
213
230
|
"MAX_INLINE_BYTES",
|
|
214
231
|
"MEDIA_LIMITS",
|
|
215
232
|
"MIMES",
|
|
233
|
+
"MediaQuestion",
|
|
234
|
+
"MediaRequest",
|
|
216
235
|
"Model",
|
|
217
236
|
"ModelList",
|
|
218
237
|
"NESTED_FUNCTION_TOOLS",
|
|
219
238
|
"NotFoundError",
|
|
239
|
+
"OrdinaryQuestion",
|
|
240
|
+
"OrdinaryRequest",
|
|
220
241
|
"OverloadedError",
|
|
221
242
|
"PayloadTooLargeError",
|
|
222
243
|
"PermissionError",
|
|
@@ -254,7 +275,10 @@ __all__ = [
|
|
|
254
275
|
"ToolHandler",
|
|
255
276
|
"Usage",
|
|
256
277
|
"VERSION",
|
|
278
|
+
"VIDEO_PROFILE",
|
|
279
|
+
"VideoAttachment",
|
|
257
280
|
"VideoNotAvailableError",
|
|
281
|
+
"VideoRequest",
|
|
258
282
|
"__version__",
|
|
259
283
|
"attachment_magic_matches",
|
|
260
284
|
"create_tool_handler",
|
|
@@ -277,5 +301,6 @@ __all__ = [
|
|
|
277
301
|
"stream_error",
|
|
278
302
|
"validate_attachments",
|
|
279
303
|
"validate_expected",
|
|
280
|
-
"validate_flow"
|
|
304
|
+
"validate_flow",
|
|
305
|
+
"video_attachment"
|
|
281
306
|
]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Credential-safe diagnostics without retaining credentials after client GC."""
|
|
2
|
+
import functools
|
|
3
|
+
import builtins
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from types import MappingProxyType, MemberDescriptorType, GetSetDescriptorType
|
|
8
|
+
from websockets.exceptions import ConnectionClosed, ConnectionClosedOK, ConnectionClosedError
|
|
9
|
+
from websockets.frames import Close
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
|
|
12
|
+
_active_key = ContextVar('system_zero_diagnostic_key', default=None)
|
|
13
|
+
_sensitive = re.compile(r'^(authorization|proxy-authorization|api[_-]?key)$', re.I)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Secret:
|
|
17
|
+
__slots__ = ('__value', '__weakref__')
|
|
18
|
+
|
|
19
|
+
def __init__(self, value):
|
|
20
|
+
self.__value = value
|
|
21
|
+
|
|
22
|
+
def reveal(self):
|
|
23
|
+
return self.__value
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return '[redacted]'
|
|
27
|
+
|
|
28
|
+
__str__ = __repr__
|
|
29
|
+
|
|
30
|
+
def __reduce_ex__(self, protocol):
|
|
31
|
+
raise TypeError('SDK credentials cannot be pickled.')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def reveal(value):
|
|
35
|
+
# Revalidation still handles an application overwriting a private attribute.
|
|
36
|
+
return value.reveal() if isinstance(value, Secret) else value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def redact_text(value, key=None):
|
|
40
|
+
key = reveal(key if key is not None else _active_key.get())
|
|
41
|
+
if key:
|
|
42
|
+
value = value.replace(key, '[redacted]')
|
|
43
|
+
return re.sub(r'\bBearer[ \t]+[A-Za-z0-9._~+/-]+=*', 'Bearer [redacted]', value, flags=re.I)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def redact(value, _seen=None, _depth=0):
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
return redact_text(value)
|
|
49
|
+
if isinstance(value, bytes):
|
|
50
|
+
clean = redact_text(value.decode('utf-8', errors='replace')).encode('utf-8')
|
|
51
|
+
return value if clean == value else clean
|
|
52
|
+
if value is None or isinstance(value, (bool, int, float, Secret)):
|
|
53
|
+
return value
|
|
54
|
+
seen = set() if _seen is None else _seen
|
|
55
|
+
if id(value) in seen or _depth > 32 or len(seen) > 4096:
|
|
56
|
+
return '[omitted]'
|
|
57
|
+
seen.add(id(value))
|
|
58
|
+
if isinstance(value, Mapping):
|
|
59
|
+
clean = {redact_text(str(k)): redact(v, seen, _depth + 1) for k, v in value.items()
|
|
60
|
+
if not _sensitive.fullmatch(str(k))}
|
|
61
|
+
return value if len(clean) == len(value) and all(k in value and v is value[k] for k, v in clean.items()) else clean
|
|
62
|
+
if isinstance(value, (tuple, list)):
|
|
63
|
+
clean = type(value)(redact(v, seen, _depth + 1) for v in value)
|
|
64
|
+
return value if all(a is b for a, b in zip(value, clean)) else clean
|
|
65
|
+
return '[omitted]'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def protect_exception(error, seen=None):
|
|
69
|
+
"""Scrub reachable native causes, including suppressed Python contexts."""
|
|
70
|
+
seen = set() if seen is None else seen
|
|
71
|
+
if id(error) in seen:
|
|
72
|
+
return Exception('Cyclic exception context omitted.')
|
|
73
|
+
if len(seen) >= 32:
|
|
74
|
+
return Exception('Further exception context omitted.')
|
|
75
|
+
seen.add(id(error))
|
|
76
|
+
# Foreign exception classes may print or pickle hidden slots through custom hooks.
|
|
77
|
+
# Keep SDK/builtin classifications; retain only plain diagnostic state otherwise.
|
|
78
|
+
module = type(error).__module__
|
|
79
|
+
close = isinstance(error, ConnectionClosed)
|
|
80
|
+
if close:
|
|
81
|
+
def frame(value):
|
|
82
|
+
return None if value is None else Close(value.code, redact_text(value.reason))
|
|
83
|
+
cls = ConnectionClosedOK if isinstance(error, ConnectionClosedOK) else ConnectionClosedError if isinstance(error, ConnectionClosedError) else ConnectionClosed
|
|
84
|
+
clean = cls(frame(error.rcvd), frame(error.sent), error.rcvd_then_sent)
|
|
85
|
+
clean.__dict__.update({name: value for name, value in vars(error).items()
|
|
86
|
+
if name not in ('rcvd', 'sent', 'rcvd_then_sent')})
|
|
87
|
+
clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
|
|
88
|
+
error = clean
|
|
89
|
+
elif module != 'builtins' and not module.startswith('system_zero.'):
|
|
90
|
+
clean = Exception(*redact(error.args))
|
|
91
|
+
clean.__dict__.update(vars(error))
|
|
92
|
+
clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
|
|
93
|
+
error = clean
|
|
94
|
+
group = isinstance(error, getattr(builtins, 'BaseExceptionGroup', ()))
|
|
95
|
+
if group:
|
|
96
|
+
children = tuple(protect_exception(child, seen) for child in error.exceptions)
|
|
97
|
+
clean = builtins.BaseExceptionGroup(redact_text(error.message), children)
|
|
98
|
+
clean.__dict__.update(vars(error))
|
|
99
|
+
clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
|
|
100
|
+
error = clean
|
|
101
|
+
elif not close:
|
|
102
|
+
error.args = redact(error.args)
|
|
103
|
+
# Built-in exception printing and pickling use native slots independently
|
|
104
|
+
# of args/__dict__ (SyntaxError, UnicodeError, AttributeError, ImportError...).
|
|
105
|
+
# Enumerate the built-in descriptors, not arbitrary foreign properties.
|
|
106
|
+
excluded = {'args', '__dict__', '__weakref__', '__traceback__', '__cause__', '__context__', 'exceptions', 'message'}
|
|
107
|
+
for cls in type(error).__mro__:
|
|
108
|
+
if cls.__module__ != 'builtins':
|
|
109
|
+
continue
|
|
110
|
+
for name, descriptor in vars(cls).items():
|
|
111
|
+
if name.startswith('__') or name in excluded or not isinstance(descriptor, (MemberDescriptorType, GetSetDescriptorType)):
|
|
112
|
+
continue
|
|
113
|
+
value = getattr(error, name, None)
|
|
114
|
+
clean = redact(value)
|
|
115
|
+
if clean is value:
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
setattr(error, name, clean)
|
|
119
|
+
except (AttributeError, TypeError):
|
|
120
|
+
# Unknown read-only native state cannot safely retain its type.
|
|
121
|
+
clean = Exception(*redact(error.args))
|
|
122
|
+
clean.__dict__.update(vars(error))
|
|
123
|
+
clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
|
|
124
|
+
error = clean
|
|
125
|
+
break
|
|
126
|
+
for name, value in list(vars(error).items()):
|
|
127
|
+
if redact_text(name) != name or _sensitive.fullmatch(name) or name in ('request', '_request', 'response', 'config'):
|
|
128
|
+
delattr(error, name)
|
|
129
|
+
elif close and name in ('rcvd', 'sent'):
|
|
130
|
+
continue
|
|
131
|
+
elif isinstance(value, BaseException):
|
|
132
|
+
setattr(error, name, protect_exception(value, seen))
|
|
133
|
+
else:
|
|
134
|
+
clean = redact(value)
|
|
135
|
+
setattr(error, name, MappingProxyType(clean) if isinstance(value, MappingProxyType) else clean)
|
|
136
|
+
for name in ('__cause__', '__context__'):
|
|
137
|
+
cause = getattr(error, name)
|
|
138
|
+
if cause is not None:
|
|
139
|
+
setattr(error, name, protect_exception(cause, seen))
|
|
140
|
+
return error
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def protected(method):
|
|
144
|
+
@functools.wraps(method)
|
|
145
|
+
def call(*args, **kwargs):
|
|
146
|
+
owner = args[0] if args else kwargs.get('client')
|
|
147
|
+
token = _active_key.set(getattr(owner, '_api_key', getattr(getattr(owner, '_client', None), '_api_key', None)))
|
|
148
|
+
try:
|
|
149
|
+
try:
|
|
150
|
+
return method(*args, **kwargs)
|
|
151
|
+
except Exception as error:
|
|
152
|
+
failure = protect_exception(error)
|
|
153
|
+
if failure is error:
|
|
154
|
+
raise
|
|
155
|
+
# Outside the except suite: do not attach the raw group as context.
|
|
156
|
+
raise failure from None
|
|
157
|
+
finally:
|
|
158
|
+
_active_key.reset(token)
|
|
159
|
+
return call
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def protected_async(method):
|
|
163
|
+
@functools.wraps(method)
|
|
164
|
+
async def call(*args, **kwargs):
|
|
165
|
+
token = _active_key.set(getattr(args[0], '_api_key', None))
|
|
166
|
+
try:
|
|
167
|
+
try:
|
|
168
|
+
return await method(*args, **kwargs)
|
|
169
|
+
except Exception as error:
|
|
170
|
+
failure = protect_exception(error)
|
|
171
|
+
if failure is error:
|
|
172
|
+
raise
|
|
173
|
+
raise failure from None
|
|
174
|
+
finally:
|
|
175
|
+
_active_key.reset(token)
|
|
176
|
+
return call
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class DiagnosticLogger(logging.LoggerAdapter):
|
|
180
|
+
"""Redact before LogRecord creation; leave application logging untouched."""
|
|
181
|
+
def __init__(self, key):
|
|
182
|
+
super().__init__(logging.getLogger('websockets.client'), {})
|
|
183
|
+
self._key = key
|
|
184
|
+
|
|
185
|
+
def log(self, level, msg, *args, **kwargs):
|
|
186
|
+
if self.isEnabledFor(level):
|
|
187
|
+
text = str(msg) % args if args else str(msg)
|
|
188
|
+
# Native exceptions may carry requests. Never attach exc_info here.
|
|
189
|
+
kwargs.pop('exc_info', None)
|
|
190
|
+
kwargs.pop('extra', None)
|
|
191
|
+
self.logger.log(level, redact_text(text, self._key), **kwargs)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _ContextFilter(logging.Filter):
|
|
195
|
+
def filter(self, record):
|
|
196
|
+
if _active_key.get() is not None:
|
|
197
|
+
record.msg = redact_text(record.getMessage())
|
|
198
|
+
record.args = ()
|
|
199
|
+
# A native traceback can retain a request with credential headers.
|
|
200
|
+
record.exc_info = record.exc_text = None
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Library loggers, not handlers or the root logger. ContextVar isolates other callers.
|
|
205
|
+
_filter = _ContextFilter()
|
|
206
|
+
for _logger_name in ('httpx', 'httpcore.connection', 'httpcore.http11', 'httpcore.http2', 'httpcore.proxy', 'httpcore.socks'):
|
|
207
|
+
logging.getLogger(_logger_name).addFilter(_filter)
|