@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.
Files changed (58) hide show
  1. package/dist/emitter/emitter.d.ts.map +1 -1
  2. package/dist/emitter/emitter.js +6 -5
  3. package/dist/emitter/emitter.js.map +1 -1
  4. package/dist/emitter/external-process.d.ts +0 -14
  5. package/dist/emitter/external-process.d.ts.map +1 -1
  6. package/dist/emitter/external-process.js +1 -17
  7. package/dist/emitter/external-process.js.map +1 -1
  8. package/dist/emitter/http.d.ts +44 -1
  9. package/dist/emitter/http.d.ts.map +1 -1
  10. package/dist/emitter/http.js +96 -0
  11. package/dist/emitter/http.js.map +1 -1
  12. package/dist/emitter/lib.d.ts +5 -5
  13. package/dist/emitter/lib.js +2 -2
  14. package/dist/emitter/lib.js.map +1 -1
  15. package/dist/emitter/yaml-utils.d.ts +19 -0
  16. package/dist/emitter/yaml-utils.d.ts.map +1 -0
  17. package/dist/emitter/yaml-utils.js +22 -0
  18. package/dist/emitter/yaml-utils.js.map +1 -0
  19. package/emitter/src/emitter.ts +7 -5
  20. package/emitter/src/external-process.ts +1 -18
  21. package/emitter/src/http.ts +154 -0
  22. package/emitter/src/lib.ts +2 -2
  23. package/emitter/src/yaml-utils.ts +22 -0
  24. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  25. package/emitter/test/emitter.test.ts +32 -0
  26. package/emitter/test/streaming.test.ts +155 -0
  27. package/emitter/test/test-host.ts +83 -0
  28. package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
  29. package/eng/scripts/ci/regenerate-common.ts +1 -5
  30. package/eng/scripts/ci/run_pylint.py +1 -0
  31. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  32. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  33. package/generator/build/lib/pygen/codegen/models/code_model.py +22 -8
  34. package/generator/build/lib/pygen/codegen/models/enum_type.py +14 -2
  35. package/generator/build/lib/pygen/codegen/models/operation.py +6 -1
  36. package/generator/build/lib/pygen/codegen/models/response.py +152 -8
  37. package/generator/build/lib/pygen/codegen/serializers/__init__.py +7 -0
  38. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +197 -58
  39. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +4 -0
  40. package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +27 -0
  41. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +40 -3
  42. package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
  43. package/generator/build/lib/pygen/preprocess/__init__.py +2 -0
  44. package/generator/component-detection-pip-report.json +5 -5
  45. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  46. package/generator/pygen/codegen/models/code_model.py +22 -8
  47. package/generator/pygen/codegen/models/enum_type.py +14 -2
  48. package/generator/pygen/codegen/models/operation.py +6 -1
  49. package/generator/pygen/codegen/models/response.py +152 -8
  50. package/generator/pygen/codegen/serializers/__init__.py +7 -0
  51. package/generator/pygen/codegen/serializers/builder_serializer.py +197 -58
  52. package/generator/pygen/codegen/serializers/general_serializer.py +4 -0
  53. package/generator/pygen/codegen/serializers/import_serializer.py +27 -0
  54. package/generator/pygen/codegen/serializers/types_serializer.py +40 -3
  55. package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
  56. package/generator/pygen/preprocess/__init__.py +2 -0
  57. package/generator/pygen.egg-info/SOURCES.txt +1 -0
  58. 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.
@@ -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
 
@@ -180,10 +180,10 @@ const libDef = {
180
180
  },
181
181
  },
182
182
  "no-sdk-clients": {
183
- severity: "error",
183
+ severity: "warning",
184
184
  messages: {
185
185
  default:
186
- "The Python emitter did not find any SDK clients in this TypeSpec program. The current Python generator expects at least one client/service to generate code.",
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
+ }