@typespec/http-client-python 0.36.0 → 0.37.1
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/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +6 -5
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/external-process.d.ts +0 -14
- package/dist/emitter/external-process.d.ts.map +1 -1
- package/dist/emitter/external-process.js +1 -17
- package/dist/emitter/external-process.js.map +1 -1
- package/dist/emitter/http.d.ts +44 -1
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +96 -0
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/lib.d.ts +5 -5
- package/dist/emitter/lib.js +2 -2
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/yaml-utils.d.ts +19 -0
- package/dist/emitter/yaml-utils.d.ts.map +1 -0
- package/dist/emitter/yaml-utils.js +22 -0
- package/dist/emitter/yaml-utils.js.map +1 -0
- package/emitter/src/emitter.ts +7 -5
- package/emitter/src/external-process.ts +1 -18
- package/emitter/src/http.ts +154 -0
- package/emitter/src/lib.ts +2 -2
- package/emitter/src/yaml-utils.ts +22 -0
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/emitter.test.ts +32 -0
- package/emitter/test/streaming.test.ts +155 -0
- package/emitter/test/test-host.ts +83 -0
- package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
- package/eng/scripts/ci/regenerate-common.ts +1 -5
- package/eng/scripts/ci/run_pylint.py +1 -0
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/generator/build/lib/pygen/codegen/models/code_model.py +22 -8
- package/generator/build/lib/pygen/codegen/models/enum_type.py +14 -2
- package/generator/build/lib/pygen/codegen/models/operation.py +6 -1
- package/generator/build/lib/pygen/codegen/models/response.py +152 -8
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +7 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +197 -58
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +4 -0
- package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +27 -0
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +40 -3
- package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/build/lib/pygen/preprocess/__init__.py +2 -0
- package/generator/component-detection-pip-report.json +5 -5
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/codegen/models/code_model.py +22 -8
- package/generator/pygen/codegen/models/enum_type.py +14 -2
- package/generator/pygen/codegen/models/operation.py +6 -1
- package/generator/pygen/codegen/models/response.py +152 -8
- package/generator/pygen/codegen/serializers/__init__.py +7 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +197 -58
- package/generator/pygen/codegen/serializers/general_serializer.py +4 -0
- package/generator/pygen/codegen/serializers/import_serializer.py +27 -0
- package/generator/pygen/codegen/serializers/types_serializer.py +40 -3
- package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/pygen/preprocess/__init__.py +2 -0
- package/generator/pygen.egg-info/SOURCES.txt +1 -0
- package/package.json +6 -6
|
@@ -3,8 +3,8 @@ import type { ChildProcess, SpawnOptions } from "child_process";
|
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
5
|
import { mkdir, writeFile } from "fs/promises";
|
|
6
|
-
import jsyaml from "js-yaml";
|
|
7
6
|
import os from "os";
|
|
7
|
+
import { dumpCodeModelToYaml } from "./yaml-utils.js";
|
|
8
8
|
|
|
9
9
|
const tspCodeGenTempDir = joinPaths(os.tmpdir(), "tsp-codegen");
|
|
10
10
|
|
|
@@ -12,23 +12,6 @@ export function createTempPath(extension: string, prefix: string = "") {
|
|
|
12
12
|
return joinPaths(tspCodeGenTempDir, prefix + randomUUID() + extension);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Serialize the given codemodel to a YAML string.
|
|
17
|
-
*
|
|
18
|
-
* The generated YAML is consumed by the Python generator, which parses it with
|
|
19
|
-
* PyYAML (YAML 1.1). js-yaml, on the other hand, dumps using YAML 1.2 rules, so
|
|
20
|
-
* plain scalars such as `2020_01_01` (a snake-cased enum member name) are left
|
|
21
|
-
* unquoted because YAML 1.2 does not treat underscores as integer separators.
|
|
22
|
-
* PyYAML would then read `2020_01_01` back as the integer `20200101`, corrupting
|
|
23
|
-
* string values (e.g. enum member names, descriptions). Forcing every string
|
|
24
|
-
* scalar to be quoted guarantees that PyYAML round-trips them as strings.
|
|
25
|
-
* @param codemodel Codemodel to serialize
|
|
26
|
-
* @return the YAML representation of the codemodel.
|
|
27
|
-
*/
|
|
28
|
-
export function dumpCodeModelToYaml(codemodel: unknown): string {
|
|
29
|
-
return jsyaml.dump(codemodel, { forceQuotes: true, quotingType: '"' });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
15
|
/**
|
|
33
16
|
* Save the given codemodel in a yaml file.
|
|
34
17
|
* @param name Name of the codemodel. To give a guide to the temp file name.
|
package/emitter/src/http.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
SdkQueryParameter,
|
|
19
19
|
SdkServiceMethod,
|
|
20
20
|
SdkServiceResponseHeader,
|
|
21
|
+
SdkSseEventMetadata,
|
|
21
22
|
SdkType,
|
|
22
23
|
} from "@azure-tools/typespec-client-generator-core";
|
|
23
24
|
import { getHttpOperationParameter, UsageFlags } from "@azure-tools/typespec-client-generator-core";
|
|
@@ -42,6 +43,158 @@ export enum ReferredByOperationTypes {
|
|
|
42
43
|
NonPagingOnly = 2,
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
type StructuredStreamKind = "jsonl" | "sse";
|
|
47
|
+
type EmittedType = ReturnType<typeof getType>;
|
|
48
|
+
|
|
49
|
+
interface StructuredStreamEvent {
|
|
50
|
+
eventType: string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Payload type for this one SSE event. For an event envelope, this is the type of the property
|
|
53
|
+
* marked `@Events.data`. Together with {@link eventType} these form the
|
|
54
|
+
* runtime dispatch table (wire event name -> model to deserialize) inside the generated
|
|
55
|
+
* `_callback`. This is a narrower type than {@link StructuredStreamingInfo.itemType}.
|
|
56
|
+
*/
|
|
57
|
+
payloadType: EmittedType;
|
|
58
|
+
/**
|
|
59
|
+
* True when this event is a `@terminalEvent` that carries a payload (a named / model event,
|
|
60
|
+
* not a bare string-constant sentinel). Such events are deserialized and yielded like any
|
|
61
|
+
* other event, and iteration stops immediately after one is yielded. Contrast with
|
|
62
|
+
* {@link StructuredStreamingInfo.terminalEvent}, the sentinel that stops without yielding.
|
|
63
|
+
*/
|
|
64
|
+
isTerminal?: boolean;
|
|
65
|
+
/** Content type of the payload, not the enclosing event. */
|
|
66
|
+
payloadContentType?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface StructuredStreamingInfo {
|
|
70
|
+
kind: StructuredStreamKind;
|
|
71
|
+
/**
|
|
72
|
+
* The aggregate stream element type used for the `Stream[T]` / `AsyncStream[T]` return
|
|
73
|
+
* annotation (a single type expression). For homogeneous JSONL this is the one model; for
|
|
74
|
+
* heterogeneous SSE this is the union of every event payload.
|
|
75
|
+
*
|
|
76
|
+
* Note the deliberate overlap with the per-event {@link StructuredStreamEvent.payloadType}: for
|
|
77
|
+
* heterogeneous SSE this union is exactly the sum of the `events[]` payload types. Both are
|
|
78
|
+
* carried because the union alone cannot recover the wire-name -> member mapping needed for
|
|
79
|
+
* dispatch, and the events list alone is not a single valid type expression for the annotation.
|
|
80
|
+
*/
|
|
81
|
+
itemType: EmittedType;
|
|
82
|
+
events?: StructuredStreamEvent[];
|
|
83
|
+
/**
|
|
84
|
+
* A bare string-constant `@terminalEvent` with no event name (e.g. `"[DONE]"`). Iteration
|
|
85
|
+
* stops when an event's `data` equals this value, and the sentinel is NOT yielded. Named /
|
|
86
|
+
* model terminal events are carried in {@link events} with `isTerminal: true` instead.
|
|
87
|
+
*/
|
|
88
|
+
terminalEvent?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether pygen can deserialize the stream item type. */
|
|
92
|
+
export function isStructuredStreamType(type: SdkType): boolean {
|
|
93
|
+
switch (type.kind) {
|
|
94
|
+
case "model":
|
|
95
|
+
case "union":
|
|
96
|
+
return true;
|
|
97
|
+
case "nullable":
|
|
98
|
+
return isStructuredStreamType(type.type);
|
|
99
|
+
default:
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getStructuredStreamKind(
|
|
105
|
+
response: SdkHttpResponse | SdkHttpErrorResponse,
|
|
106
|
+
): StructuredStreamKind | undefined {
|
|
107
|
+
if (response.sseMetadata) return "sse";
|
|
108
|
+
|
|
109
|
+
const contentTypes = response.streamMetadata?.contentTypes ?? response.contentTypes ?? [];
|
|
110
|
+
for (const contentType of contentTypes) {
|
|
111
|
+
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
112
|
+
if (mediaType === "text/event-stream") return "sse";
|
|
113
|
+
if (mediaType === "application/jsonl") return "jsonl";
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getStringConstantValue(type: SdkType): string | undefined {
|
|
119
|
+
if (type.kind === "nullable") return getStringConstantValue(type.type);
|
|
120
|
+
return type.kind === "constant" && typeof type.value === "string" ? type.value : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Split the SSE events into the runtime dispatch table and a bare string-constant sentinel.
|
|
125
|
+
*
|
|
126
|
+
* A `@terminalEvent` comes in two shapes:
|
|
127
|
+
* * a nameless string constant (e.g. `"[DONE]"`) -> a pure sentinel: iteration stops when an
|
|
128
|
+
* event's `data` equals this value and the event is NOT yielded. Returned as `terminalEvent`.
|
|
129
|
+
* * a named / model event (e.g. `error`, `response.completed`) -> carries a payload the consumer
|
|
130
|
+
* needs, so it is deserialized and yielded like any other event, then iteration stops. Returned
|
|
131
|
+
* in `events` with `isTerminal: true`.
|
|
132
|
+
*
|
|
133
|
+
* `toPayloadType` maps event payloads to emitted types; it is injected so this partitioning stays
|
|
134
|
+
* a pure function that can be unit-tested without a full emitter context.
|
|
135
|
+
*/
|
|
136
|
+
export function partitionSSEEvents(
|
|
137
|
+
events: readonly SdkSseEventMetadata[],
|
|
138
|
+
toPayloadType: (type: SdkType) => EmittedType,
|
|
139
|
+
): { events: StructuredStreamEvent[]; terminalEvent?: string } {
|
|
140
|
+
const dispatch: StructuredStreamEvent[] = [];
|
|
141
|
+
let terminalEvent: string | undefined;
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
if (event.isTerminalEvent) {
|
|
144
|
+
const sentinelValue =
|
|
145
|
+
event.eventType === undefined
|
|
146
|
+
? (getStringConstantValue(event.payloadType) ?? getStringConstantValue(event.type))
|
|
147
|
+
: undefined;
|
|
148
|
+
if (sentinelValue !== undefined) {
|
|
149
|
+
// Keep the first sentinel; no current spec defines more than one.
|
|
150
|
+
terminalEvent ??= sentinelValue;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
dispatch.push({
|
|
154
|
+
eventType: event.eventType,
|
|
155
|
+
payloadType: toPayloadType(event.payloadType),
|
|
156
|
+
isTerminal: true,
|
|
157
|
+
payloadContentType: event.payloadContentType,
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
dispatch.push({
|
|
162
|
+
eventType: event.eventType,
|
|
163
|
+
payloadType: toPayloadType(event.payloadType),
|
|
164
|
+
payloadContentType: event.payloadContentType,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return terminalEvent !== undefined ? { events: dispatch, terminalEvent } : { events: dispatch };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function emitStructuredStreamingInfo(
|
|
171
|
+
context: PythonSdkContext,
|
|
172
|
+
response: SdkHttpResponse | SdkHttpErrorResponse,
|
|
173
|
+
): StructuredStreamingInfo | undefined {
|
|
174
|
+
const streamMetadata = response.streamMetadata;
|
|
175
|
+
if (!streamMetadata || !isStructuredStreamType(streamMetadata.streamType)) return undefined;
|
|
176
|
+
|
|
177
|
+
const kind = getStructuredStreamKind(response);
|
|
178
|
+
if (!kind) return undefined;
|
|
179
|
+
|
|
180
|
+
const streaming: StructuredStreamingInfo = {
|
|
181
|
+
kind,
|
|
182
|
+
itemType: getType(context, streamMetadata.streamType),
|
|
183
|
+
};
|
|
184
|
+
if (kind !== "sse") return streaming;
|
|
185
|
+
|
|
186
|
+
const sseMetadata = response.sseMetadata;
|
|
187
|
+
if (!sseMetadata || sseMetadata.events.length === 0) return undefined;
|
|
188
|
+
|
|
189
|
+
const { events, terminalEvent } = partitionSSEEvents(sseMetadata.events, (type) =>
|
|
190
|
+
getType(context, type),
|
|
191
|
+
);
|
|
192
|
+
if (events.length > 0) streaming.events = events;
|
|
193
|
+
if (terminalEvent !== undefined) streaming.terminalEvent = terminalEvent;
|
|
194
|
+
|
|
195
|
+
return streaming;
|
|
196
|
+
}
|
|
197
|
+
|
|
45
198
|
function isEtagType(type: SdkType): boolean {
|
|
46
199
|
if (type.kind === "nullable") return isEtagType(type.type);
|
|
47
200
|
const raw = type.__raw;
|
|
@@ -682,6 +835,7 @@ function emitHttpResponse(
|
|
|
682
835
|
"invalid-lro-result",
|
|
683
836
|
method,
|
|
684
837
|
),
|
|
838
|
+
streaming: isException ? undefined : emitStructuredStreamingInfo(context, response),
|
|
685
839
|
};
|
|
686
840
|
}
|
|
687
841
|
|
package/emitter/src/lib.ts
CHANGED
|
@@ -180,10 +180,10 @@ const libDef = {
|
|
|
180
180
|
},
|
|
181
181
|
},
|
|
182
182
|
"no-sdk-clients": {
|
|
183
|
-
severity: "
|
|
183
|
+
severity: "warning",
|
|
184
184
|
messages: {
|
|
185
185
|
default:
|
|
186
|
-
"The Python emitter did not find any SDK clients in this TypeSpec program.
|
|
186
|
+
"The Python emitter did not find any SDK clients in this TypeSpec program. No client code will be generated (models can still be emitted). Suppress this warning if this is expected.",
|
|
187
187
|
},
|
|
188
188
|
},
|
|
189
189
|
"browser-runtime-load-failed": {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import jsyaml from "js-yaml";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serialize the given codemodel to a YAML string.
|
|
5
|
+
*
|
|
6
|
+
* The generated YAML is consumed by the Python generator, which parses it with
|
|
7
|
+
* PyYAML (YAML 1.1). js-yaml, on the other hand, dumps using YAML 1.2 rules, so
|
|
8
|
+
* plain scalars such as `2020_01_01` (a snake-cased enum member name) are left
|
|
9
|
+
* unquoted because YAML 1.2 does not treat underscores as integer separators.
|
|
10
|
+
* PyYAML would then read `2020_01_01` back as the integer `20200101`, corrupting
|
|
11
|
+
* string values (e.g. enum member names, descriptions). Forcing every string
|
|
12
|
+
* scalar to be quoted guarantees that PyYAML round-trips them as strings.
|
|
13
|
+
*
|
|
14
|
+
* This module must remain browser-safe (no Node built-ins): `emitter.ts` calls
|
|
15
|
+
* this function on the browser/Pyodide path, and esbuild bundles it into the
|
|
16
|
+
* playground bundle with `platform: "browser"`.
|
|
17
|
+
* @param codemodel Codemodel to serialize
|
|
18
|
+
* @return the YAML representation of the codemodel.
|
|
19
|
+
*/
|
|
20
|
+
export function dumpCodeModelToYaml(codemodel: unknown): string {
|
|
21
|
+
return jsyaml.dump(codemodel, { forceQuotes: true, quotingType: '"' });
|
|
22
|
+
}
|