@typespec/http-client-python 0.35.1 → 0.37.0
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/README.md +6 -0
- package/dist/emitter/code-model.d.ts +1 -1
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/emitter.d.ts +2 -2
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +24 -3
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/external-process.d.ts +1 -15
- 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 +45 -2
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +97 -1
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/index.d.ts +2 -1
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/lib.d.ts +3 -2
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +6 -1
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/node-runner.browser.d.ts +1 -1
- package/dist/emitter/node-runner.browser.d.ts.map +1 -1
- package/dist/emitter/node-runner.browser.js.map +1 -1
- package/dist/emitter/node-runner.d.ts +3 -3
- package/dist/emitter/node-runner.d.ts.map +1 -1
- package/dist/emitter/node-runner.js.map +1 -1
- package/dist/emitter/system-requirements.d.ts.map +1 -1
- package/dist/emitter/system-requirements.js.map +1 -1
- package/dist/emitter/types.d.ts +4 -4
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +15 -3
- package/dist/emitter/types.js.map +1 -1
- package/dist/emitter/utils.d.ts +2 -2
- package/dist/emitter/utils.d.ts.map +1 -1
- package/dist/emitter/utils.js +1 -1
- package/dist/emitter/utils.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/code-model.ts +4 -2
- package/emitter/src/emitter.ts +31 -7
- package/emitter/src/external-process.ts +3 -19
- package/emitter/src/http.ts +159 -5
- package/emitter/src/index.ts +2 -1
- package/emitter/src/lib.ts +11 -6
- package/emitter/src/node-runner.browser.ts +2 -1
- package/emitter/src/node-runner.ts +6 -3
- package/emitter/src/system-requirements.ts +2 -1
- package/emitter/src/types.ts +23 -8
- package/emitter/src/utils.ts +5 -4
- package/emitter/src/yaml-utils.ts +22 -0
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/browser-pyodide.test.ts +25 -0
- package/emitter/test/streaming.test.ts +155 -0
- package/emitter/test/types.test.ts +72 -0
- package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
- package/eng/scripts/ci/dev_requirements.txt +1 -1
- package/eng/scripts/ci/regenerate-common.ts +1 -1
- package/eng/scripts/ci/regenerate.ts +1 -2
- package/eng/scripts/ci/run-tests.ts +2 -1
- 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/eng/scripts/setup/system-requirements.ts +2 -1
- package/generator/build/lib/pygen/__init__.py +24 -5
- package/generator/build/lib/pygen/codegen/models/__init__.py +3 -1
- package/generator/build/lib/pygen/codegen/models/code_model.py +29 -9
- package/generator/build/lib/pygen/codegen/models/enum_type.py +21 -9
- package/generator/build/lib/pygen/codegen/models/list_type.py +1 -0
- package/generator/build/lib/pygen/codegen/models/model_type.py +1 -3
- package/generator/build/lib/pygen/codegen/models/operation.py +13 -3
- package/generator/build/lib/pygen/codegen/models/parameter.py +3 -1
- package/generator/build/lib/pygen/codegen/models/response.py +152 -8
- package/generator/build/lib/pygen/codegen/models/utils.py +14 -0
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +13 -6
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +219 -69
- package/generator/build/lib/pygen/codegen/serializers/client_serializer.py +13 -6
- 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/model_serializer.py +4 -1
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +44 -12
- package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/build/lib/pygen/preprocess/__init__.py +70 -17
- package/generator/build/lib/pygen/utils.py +15 -0
- package/generator/component-detection-pip-report.json +9 -9
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +24 -5
- package/generator/pygen/codegen/models/__init__.py +3 -1
- package/generator/pygen/codegen/models/code_model.py +29 -9
- package/generator/pygen/codegen/models/enum_type.py +21 -9
- package/generator/pygen/codegen/models/list_type.py +1 -0
- package/generator/pygen/codegen/models/model_type.py +1 -3
- package/generator/pygen/codegen/models/operation.py +13 -3
- package/generator/pygen/codegen/models/parameter.py +3 -1
- package/generator/pygen/codegen/models/response.py +152 -8
- package/generator/pygen/codegen/models/utils.py +14 -0
- package/generator/pygen/codegen/serializers/__init__.py +13 -6
- package/generator/pygen/codegen/serializers/builder_serializer.py +219 -69
- package/generator/pygen/codegen/serializers/client_serializer.py +13 -6
- 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/model_serializer.py +4 -1
- package/generator/pygen/codegen/serializers/types_serializer.py +44 -12
- package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/pygen/preprocess/__init__.py +70 -17
- package/generator/pygen/utils.py +15 -0
- package/generator/pygen.egg-info/PKG-INFO +1 -1
- package/generator/pygen.egg-info/SOURCES.txt +1 -0
- package/generator/pygen.egg-info/requires.txt +1 -1
- package/generator/setup.py +1 -1
- package/package.json +33 -33
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { strictEqual } from "assert";
|
|
2
|
+
import { afterEach, describe, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const loadPyodide = vi.hoisted(() => vi.fn());
|
|
5
|
+
|
|
6
|
+
vi.mock("../src/pyodide-loader.js", () => ({ loadPyodide }));
|
|
7
|
+
|
|
8
|
+
describe("typespec-python: browser pyodide bootstrap", () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
delete (globalThis as any).window;
|
|
11
|
+
loadPyodide.mockReset();
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Hosts like the TypeSpec playground import every available emitter up front. Booting Pyodide at
|
|
16
|
+
// module scope downloaded a full CPython WebAssembly runtime on every page load, which pushed the
|
|
17
|
+
// page past the per-tab memory budget on mobile browsers and prevented it from loading.
|
|
18
|
+
it("does not boot pyodide when the emitter is imported in a browser", async () => {
|
|
19
|
+
(globalThis as any).window = globalThis;
|
|
20
|
+
|
|
21
|
+
await import("../src/emitter.js");
|
|
22
|
+
|
|
23
|
+
strictEqual(loadPyodide.mock.calls.length, 0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { strictEqual } from "assert";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
getStructuredStreamKind,
|
|
5
|
+
isStructuredStreamType,
|
|
6
|
+
partitionSSEEvents,
|
|
7
|
+
} from "../src/http.js";
|
|
8
|
+
|
|
9
|
+
describe("typespec-python: structured streaming", () => {
|
|
10
|
+
it("treats model and union payloads as structured", () => {
|
|
11
|
+
strictEqual(isStructuredStreamType({ kind: "model" } as any), true);
|
|
12
|
+
strictEqual(isStructuredStreamType({ kind: "union" } as any), true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("unwraps nullable payloads", () => {
|
|
16
|
+
strictEqual(isStructuredStreamType({ kind: "nullable", type: { kind: "model" } } as any), true);
|
|
17
|
+
strictEqual(
|
|
18
|
+
isStructuredStreamType({ kind: "nullable", type: { kind: "bytes" } } as any),
|
|
19
|
+
false,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("treats bare byte/string payloads as unstructured", () => {
|
|
24
|
+
strictEqual(isStructuredStreamType({ kind: "bytes" } as any), false);
|
|
25
|
+
strictEqual(isStructuredStreamType({ kind: "string" } as any), false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("detects the stream protocol explicitly", () => {
|
|
29
|
+
strictEqual(getStructuredStreamKind({ sseMetadata: { events: [] } } as any), "sse");
|
|
30
|
+
strictEqual(
|
|
31
|
+
getStructuredStreamKind({
|
|
32
|
+
streamMetadata: { contentTypes: ["text/event-stream; charset=utf-8"] },
|
|
33
|
+
} as any),
|
|
34
|
+
"sse",
|
|
35
|
+
);
|
|
36
|
+
strictEqual(
|
|
37
|
+
getStructuredStreamKind({
|
|
38
|
+
streamMetadata: { contentTypes: ["application/jsonl"] },
|
|
39
|
+
} as any),
|
|
40
|
+
"jsonl",
|
|
41
|
+
);
|
|
42
|
+
strictEqual(
|
|
43
|
+
getStructuredStreamKind({
|
|
44
|
+
streamMetadata: { contentTypes: ["application/json"] },
|
|
45
|
+
} as any),
|
|
46
|
+
undefined,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("terminal-event partitioning", () => {
|
|
51
|
+
const identity = (payloadType: any) => payloadType;
|
|
52
|
+
const model = (name: string) => ({ kind: "model", name });
|
|
53
|
+
const constant = (value: string) => ({ kind: "constant", value });
|
|
54
|
+
|
|
55
|
+
it("keeps a nameless string-constant `[DONE]` as a drop-and-stop sentinel", () => {
|
|
56
|
+
const created = model("ResponseCreated");
|
|
57
|
+
const done = constant("[DONE]");
|
|
58
|
+
const { events, terminalEvent } = partitionSSEEvents(
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
eventType: "response.created",
|
|
62
|
+
isTerminalEvent: false,
|
|
63
|
+
type: created,
|
|
64
|
+
payloadType: created,
|
|
65
|
+
},
|
|
66
|
+
{ eventType: undefined, isTerminalEvent: true, type: done, payloadType: done },
|
|
67
|
+
] as any,
|
|
68
|
+
identity,
|
|
69
|
+
);
|
|
70
|
+
// The sentinel is NOT a dispatch event; it only sets `terminalEvent`.
|
|
71
|
+
strictEqual(terminalEvent, "[DONE]");
|
|
72
|
+
strictEqual(events.length, 1);
|
|
73
|
+
strictEqual(events[0].eventType, "response.created");
|
|
74
|
+
strictEqual(events[0].isTerminal, undefined);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("keeps named / model `@terminalEvent`s in the dispatch table as yield-and-stop events", () => {
|
|
78
|
+
const created = model("ResponseCreated");
|
|
79
|
+
const completed = model("ResponseCompleted");
|
|
80
|
+
const errored = model("StreamError");
|
|
81
|
+
const { events, terminalEvent } = partitionSSEEvents(
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
eventType: "response.created",
|
|
85
|
+
isTerminalEvent: false,
|
|
86
|
+
type: created,
|
|
87
|
+
payloadType: created,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
eventType: "response.completed",
|
|
91
|
+
isTerminalEvent: true,
|
|
92
|
+
type: completed,
|
|
93
|
+
payloadType: completed,
|
|
94
|
+
},
|
|
95
|
+
{ eventType: "error", isTerminalEvent: true, type: errored, payloadType: errored },
|
|
96
|
+
] as any,
|
|
97
|
+
identity,
|
|
98
|
+
);
|
|
99
|
+
// No bare sentinel: the two terminals carry payloads, so they stay in `events`.
|
|
100
|
+
strictEqual(terminalEvent, undefined);
|
|
101
|
+
strictEqual(events.length, 3);
|
|
102
|
+
strictEqual(events[0].isTerminal, undefined);
|
|
103
|
+
strictEqual(events[1].eventType, "response.completed");
|
|
104
|
+
strictEqual(events[1].isTerminal, true);
|
|
105
|
+
strictEqual(events[1].payloadType, completed);
|
|
106
|
+
strictEqual(events[2].eventType, "error");
|
|
107
|
+
strictEqual(events[2].isTerminal, true);
|
|
108
|
+
strictEqual(events[2].payloadType, errored);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("supports a sentinel and named terminals together", () => {
|
|
112
|
+
const delta = model("ResponseDelta");
|
|
113
|
+
const completed = model("ResponseCompleted");
|
|
114
|
+
const done = constant("[DONE]");
|
|
115
|
+
const { events, terminalEvent } = partitionSSEEvents(
|
|
116
|
+
[
|
|
117
|
+
{ eventType: "response.delta", isTerminalEvent: false, type: delta, payloadType: delta },
|
|
118
|
+
{
|
|
119
|
+
eventType: "response.completed",
|
|
120
|
+
isTerminalEvent: true,
|
|
121
|
+
type: completed,
|
|
122
|
+
payloadType: completed,
|
|
123
|
+
},
|
|
124
|
+
{ eventType: undefined, isTerminalEvent: true, type: done, payloadType: done },
|
|
125
|
+
] as any,
|
|
126
|
+
identity,
|
|
127
|
+
);
|
|
128
|
+
strictEqual(terminalEvent, "[DONE]");
|
|
129
|
+
strictEqual(events.length, 2);
|
|
130
|
+
strictEqual(events[0].isTerminal, undefined);
|
|
131
|
+
strictEqual(events[1].eventType, "response.completed");
|
|
132
|
+
strictEqual(events[1].isTerminal, true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("emits the payload metadata for event envelopes", () => {
|
|
136
|
+
const envelope = model("Envelope");
|
|
137
|
+
const payload = { kind: "string" };
|
|
138
|
+
const { events } = partitionSSEEvents(
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
eventType: "withEnvelope",
|
|
142
|
+
isTerminalEvent: false,
|
|
143
|
+
type: envelope,
|
|
144
|
+
payloadType: payload,
|
|
145
|
+
isEventEnvelope: true,
|
|
146
|
+
payloadContentType: "text/plain",
|
|
147
|
+
},
|
|
148
|
+
] as any,
|
|
149
|
+
identity,
|
|
150
|
+
);
|
|
151
|
+
strictEqual(events[0].payloadType, payload);
|
|
152
|
+
strictEqual(events[0].payloadContentType, "text/plain");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SdkBuiltInType } from "@azure-tools/typespec-client-generator-core";
|
|
2
|
+
import { deepStrictEqual } from "assert";
|
|
3
|
+
import { describe, it } from "vitest";
|
|
4
|
+
import type { PythonSdkContext } from "../src/lib.js";
|
|
5
|
+
import { getType } from "../src/types.js";
|
|
6
|
+
|
|
7
|
+
function createBuiltInType(
|
|
8
|
+
kind: SdkBuiltInType["kind"],
|
|
9
|
+
encode?: string,
|
|
10
|
+
wireType?: SdkBuiltInType,
|
|
11
|
+
): SdkBuiltInType {
|
|
12
|
+
return {
|
|
13
|
+
kind,
|
|
14
|
+
name: kind,
|
|
15
|
+
crossLanguageDefinitionId: `TypeSpec.${kind}`,
|
|
16
|
+
decorators: [],
|
|
17
|
+
encode,
|
|
18
|
+
wireType,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function emitBuiltInType(type: SdkBuiltInType): Record<string, any> {
|
|
23
|
+
const context = {
|
|
24
|
+
__simpleTypesMap: new Map(),
|
|
25
|
+
} as PythonSdkContext;
|
|
26
|
+
return getType(context, type);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("typespec-python: built-in types", () => {
|
|
30
|
+
it("preserves boolean encoded as string", () => {
|
|
31
|
+
deepStrictEqual(
|
|
32
|
+
emitBuiltInType(createBuiltInType("boolean", "string", createBuiltInType("string"))),
|
|
33
|
+
{
|
|
34
|
+
type: "boolean",
|
|
35
|
+
encode: "string",
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.each(["safeint", "uint32", "uint8"] as const)(
|
|
41
|
+
"preserves %s encoded as string as an integer",
|
|
42
|
+
(kind) => {
|
|
43
|
+
deepStrictEqual(
|
|
44
|
+
emitBuiltInType(createBuiltInType(kind, "string", createBuiltInType("string"))),
|
|
45
|
+
{
|
|
46
|
+
type: "integer",
|
|
47
|
+
encode: "string",
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
it.each(["base64", "base64url"] as const)("preserves bytes encoded as %s", (encode) => {
|
|
54
|
+
deepStrictEqual(
|
|
55
|
+
emitBuiltInType(createBuiltInType("bytes", encode, createBuiltInType("string"))),
|
|
56
|
+
{
|
|
57
|
+
type: "bytes",
|
|
58
|
+
encode,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("falls back to the wire type for a custom encoding", () => {
|
|
64
|
+
deepStrictEqual(
|
|
65
|
+
emitBuiltInType(createBuiltInType("string", "abc", createBuiltInType("int32"))),
|
|
66
|
+
{
|
|
67
|
+
type: "integer",
|
|
68
|
+
encode: "abc",
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ok, strictEqual } from "assert";
|
|
2
2
|
import { load } from "js-yaml";
|
|
3
3
|
import { describe, it } from "vitest";
|
|
4
|
-
import { dumpCodeModelToYaml } from "../src/
|
|
4
|
+
import { dumpCodeModelToYaml } from "../src/yaml-utils.js";
|
|
5
5
|
|
|
6
|
-
describe("typespec-python:
|
|
6
|
+
describe("typespec-python: yaml-utils", () => {
|
|
7
7
|
// The Python generator parses the emitted YAML with PyYAML (YAML 1.1), where a plain
|
|
8
8
|
// scalar such as `2020_01_01` is interpreted as the integer `20200101`. js-yaml dumps
|
|
9
9
|
// using YAML 1.2 rules and would otherwise leave such string scalars unquoted, so we
|
|
@@ -320,7 +320,7 @@ export const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<str
|
|
|
320
320
|
{
|
|
321
321
|
"package-name": "typetest-model-usage-typeddictonly",
|
|
322
322
|
namespace: "typetest.model.usage.typeddictonly",
|
|
323
|
-
"models-mode": "
|
|
323
|
+
"models-mode": "none",
|
|
324
324
|
},
|
|
325
325
|
],
|
|
326
326
|
"type/model/visibility": [
|
|
@@ -20,13 +20,12 @@ import pc from "picocolors";
|
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import { parseArgs } from "util";
|
|
22
22
|
|
|
23
|
+
import type { RegenerateContext, RegenerateFlags } from "./regenerate-common.js";
|
|
23
24
|
import {
|
|
24
25
|
buildTaskGroups,
|
|
25
26
|
cleanGeneratedCode,
|
|
26
27
|
getSubdirectories,
|
|
27
28
|
preprocess,
|
|
28
|
-
RegenerateContext,
|
|
29
|
-
RegenerateFlags,
|
|
30
29
|
runParallel,
|
|
31
30
|
} from "./regenerate-common.js";
|
|
32
31
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import { ChildProcess
|
|
2
|
+
import type { ChildProcess } from "child_process";
|
|
3
|
+
import { spawn } from "child_process";
|
|
3
4
|
import fs from "fs";
|
|
4
5
|
import { cpus } from "os";
|
|
5
6
|
import { dirname, join } from "path";
|
|
@@ -52,6 +52,7 @@ def _single_dir_pylint(mod):
|
|
|
52
52
|
# narrow as possible: only this package, only the offending check.
|
|
53
53
|
per_package_disables = {
|
|
54
54
|
"azure-client-generator-core-exact-name": ["client-incorrect-naming-convention"],
|
|
55
|
+
"azure-resource-manager-operation-templates": ["too-many-statements"],
|
|
55
56
|
}
|
|
56
57
|
extra_disables = per_package_disables.get(mod.name)
|
|
57
58
|
if extra_disables:
|
|
Binary file
|
|
Binary file
|
|
@@ -12,7 +12,7 @@ from abc import ABC, abstractmethod
|
|
|
12
12
|
from typing import Any, Iterator, Optional, Union
|
|
13
13
|
|
|
14
14
|
import yaml
|
|
15
|
-
from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE
|
|
15
|
+
from .utils import TYPESPEC_PACKAGE_MODE, VALID_PACKAGE_MODE, is_typeddict_only
|
|
16
16
|
|
|
17
17
|
from ._version import VERSION
|
|
18
18
|
|
|
@@ -36,6 +36,7 @@ class OptionsDict(MutableMapping):
|
|
|
36
36
|
"low-level-client": False,
|
|
37
37
|
"no-async": False,
|
|
38
38
|
"no-namespace-folders": False,
|
|
39
|
+
"generate-typeddict": True,
|
|
39
40
|
"polymorphic-examples": 5,
|
|
40
41
|
"validate-versioning": True,
|
|
41
42
|
"version-tolerant": True,
|
|
@@ -45,10 +46,20 @@ class OptionsDict(MutableMapping):
|
|
|
45
46
|
|
|
46
47
|
def __init__(self, options: Optional[dict[str, Any]] = None) -> None:
|
|
47
48
|
self._data = options.copy() if options else {}
|
|
49
|
+
# 'models-mode: typeddict' is deprecated. Represent it internally as 'none' models-mode with
|
|
50
|
+
# TypedDict generation enabled, so the rest of the codebase reasons in terms of models-mode
|
|
51
|
+
# plus the generate-typeddict flag.
|
|
52
|
+
if self._data.get("models-mode") == "typeddict":
|
|
53
|
+
self._data["generate-typeddict"] = True
|
|
48
54
|
for key in list(self._data):
|
|
49
55
|
self._data[key] = self._validate_and_transform(key, self._data[key])
|
|
50
56
|
self._validate_combinations()
|
|
51
57
|
|
|
58
|
+
@property
|
|
59
|
+
def generate_typeddict_only(self) -> bool:
|
|
60
|
+
"""Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
|
|
61
|
+
return is_typeddict_only(self)
|
|
62
|
+
|
|
52
63
|
def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
|
|
53
64
|
if key == "head-as-boolean" and self.get("azure-arm"):
|
|
54
65
|
# override to always true if azure-arm is set
|
|
@@ -167,14 +178,22 @@ class OptionsDict(MutableMapping):
|
|
|
167
178
|
if key == "builders-visibility" and value not in ["public", "hidden", "embedded"]:
|
|
168
179
|
raise ValueError("The value of --builders-visibility must be either 'public', 'hidden', or 'embedded'")
|
|
169
180
|
|
|
181
|
+
if key == "models-mode" and value == "typeddict":
|
|
182
|
+
# Deprecated: keep accepting it for back-compat but store it as 'none' (falsy) with
|
|
183
|
+
# TypedDict generation enabled (see OptionsDict.__init__ and generate_typeddict_only).
|
|
184
|
+
_LOGGER.warning(
|
|
185
|
+
"'models-mode: typeddict' is deprecated. Use 'models-mode: none' with "
|
|
186
|
+
"'generate-typeddict: true' (the default) instead."
|
|
187
|
+
)
|
|
188
|
+
value = False
|
|
189
|
+
|
|
170
190
|
if key == "models-mode" and value == "none":
|
|
171
191
|
value = False # switch to falsy value for easier code writing
|
|
172
192
|
|
|
173
|
-
if key == "models-mode" and value not in ["msrest", "dpg",
|
|
193
|
+
if key == "models-mode" and value not in ["msrest", "dpg", False]:
|
|
174
194
|
raise ValueError(
|
|
175
|
-
"--models-mode can only be '
|
|
176
|
-
"Pass in '
|
|
177
|
-
"'none' if you don't want any."
|
|
195
|
+
"--models-mode can only be 'dpg' or 'none'. "
|
|
196
|
+
"Pass in 'dpg' for DPG models, or 'none' if you don't want any."
|
|
178
197
|
)
|
|
179
198
|
if key == "package-mode":
|
|
180
199
|
if (
|
|
@@ -169,7 +169,9 @@ def build_type(yaml_data: dict[str, Any], code_model: CodeModel) -> BaseType:
|
|
|
169
169
|
# need to special case model to avoid recursion
|
|
170
170
|
if yaml_data["base"] == "typeddict":
|
|
171
171
|
model_type = TypedDictModelType # type: ignore
|
|
172
|
-
elif yaml_data["base"] == "json" or
|
|
172
|
+
elif yaml_data["base"] == "json" or (
|
|
173
|
+
not code_model.options["models-mode"] and not code_model.generate_typeddict_only
|
|
174
|
+
):
|
|
173
175
|
model_type = JSONModelType
|
|
174
176
|
elif yaml_data["base"] == "dpg":
|
|
175
177
|
model_type = DPGModelType # type: ignore
|
|
@@ -17,6 +17,7 @@ from .operation_group import OperationGroup
|
|
|
17
17
|
from .utils import NamespaceType
|
|
18
18
|
from .._utils import DEFAULT_HEADER_TEXT, DEFAULT_LICENSE_DESCRIPTION
|
|
19
19
|
from ... import OptionsDict
|
|
20
|
+
from ...utils import is_typeddict_only
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _is_legacy(options) -> bool:
|
|
@@ -89,7 +90,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
89
90
|
self.clients: list[Client] = [
|
|
90
91
|
Client.from_yaml(client_yaml_data, self) for client_yaml_data in yaml_data["clients"]
|
|
91
92
|
]
|
|
92
|
-
if self.options["models-mode"] and self.model_types:
|
|
93
|
+
if (self.options["models-mode"] or self.generate_typeddict_only) and self.model_types:
|
|
93
94
|
self.sort_model_types()
|
|
94
95
|
self.named_unions: list[CombinedType] = [
|
|
95
96
|
t for t in self.types_map.values() if isinstance(t, CombinedType) and t.name
|
|
@@ -174,6 +175,11 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
174
175
|
def get_unique_types_alias(self, serialize_namespace: str, imported_namespace: str) -> str:
|
|
175
176
|
return self._get_unique_import_alias(serialize_namespace, imported_namespace, "types")
|
|
176
177
|
|
|
178
|
+
@property
|
|
179
|
+
def generate_typeddict_only(self) -> bool:
|
|
180
|
+
"""Whether this is TypedDict-only generation ('models-mode: none' + TypedDicts)."""
|
|
181
|
+
return is_typeddict_only(self.options)
|
|
182
|
+
|
|
177
183
|
@property
|
|
178
184
|
def client_namespace_types(self) -> dict[str, ClientNamespaceType]:
|
|
179
185
|
if not self._client_namespace_types:
|
|
@@ -273,12 +279,22 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
273
279
|
self.need_utils_utils(async_mode, client_namespace)
|
|
274
280
|
or self.need_utils_serialization
|
|
275
281
|
or self.options["models-mode"] == "dpg"
|
|
282
|
+
or self.has_structured_stream
|
|
276
283
|
)
|
|
277
284
|
|
|
278
285
|
@property
|
|
279
286
|
def need_utils_serialization(self) -> bool:
|
|
280
287
|
return not self.options["client-side-validation"]
|
|
281
288
|
|
|
289
|
+
@property
|
|
290
|
+
def has_structured_stream(self) -> bool:
|
|
291
|
+
return any(
|
|
292
|
+
op.has_structured_stream_response
|
|
293
|
+
for client in self.clients
|
|
294
|
+
for og in client.operation_groups
|
|
295
|
+
for op in og.operations
|
|
296
|
+
)
|
|
297
|
+
|
|
282
298
|
def need_utils_utils(self, async_mode: bool, client_namespace: str) -> bool:
|
|
283
299
|
return (
|
|
284
300
|
self.need_utils_form_data(async_mode, client_namespace)
|
|
@@ -389,22 +405,26 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
389
405
|
def _sort_model_types_helper(
|
|
390
406
|
self,
|
|
391
407
|
current: ModelType,
|
|
392
|
-
|
|
408
|
+
seen_schema_keys: set[tuple[str, str]],
|
|
393
409
|
seen_schema_yaml_ids: set[int],
|
|
394
410
|
):
|
|
395
411
|
if current.id in seen_schema_yaml_ids:
|
|
396
412
|
return []
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
current_schema_key = (current.client_namespace, current.name)
|
|
414
|
+
if current_schema_key in seen_schema_keys:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
f"We have already generated a schema with name {current.name} "
|
|
417
|
+
f"in namespace {current.client_namespace}"
|
|
418
|
+
)
|
|
399
419
|
ancestors = [current]
|
|
400
420
|
if current.parents:
|
|
401
421
|
for parent in current.parents:
|
|
402
422
|
if parent.id in seen_schema_yaml_ids:
|
|
403
423
|
continue
|
|
404
|
-
|
|
424
|
+
seen_schema_keys.add(current_schema_key)
|
|
405
425
|
seen_schema_yaml_ids.add(current.id)
|
|
406
|
-
ancestors = self._sort_model_types_helper(parent,
|
|
407
|
-
|
|
426
|
+
ancestors = self._sort_model_types_helper(parent, seen_schema_keys, seen_schema_yaml_ids) + ancestors
|
|
427
|
+
seen_schema_keys.add(current_schema_key)
|
|
408
428
|
seen_schema_yaml_ids.add(current.id)
|
|
409
429
|
return ancestors
|
|
410
430
|
|
|
@@ -414,11 +434,11 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
|
|
|
414
434
|
:return: None
|
|
415
435
|
:rtype: None
|
|
416
436
|
"""
|
|
417
|
-
|
|
437
|
+
seen_schema_keys: set[tuple[str, str]] = set()
|
|
418
438
|
seen_schema_yaml_ids: set[int] = set()
|
|
419
439
|
sorted_object_schemas: list[ModelType] = []
|
|
420
440
|
for schema in sorted(self.model_types, key=lambda x: x.name.lower()):
|
|
421
|
-
sorted_object_schemas.extend(self._sort_model_types_helper(schema,
|
|
441
|
+
sorted_object_schemas.extend(self._sort_model_types_helper(schema, seen_schema_keys, seen_schema_yaml_ids))
|
|
422
442
|
self.model_types = sorted_object_schemas
|
|
423
443
|
|
|
424
444
|
@property
|
|
@@ -40,7 +40,7 @@ class EnumValue(BaseType):
|
|
|
40
40
|
|
|
41
41
|
def type_annotation(self, **kwargs: Any) -> str:
|
|
42
42
|
"""The python type used for type annotation"""
|
|
43
|
-
if self.code_model.
|
|
43
|
+
if self.code_model.generate_typeddict_only:
|
|
44
44
|
# A single constant enum value must be
|
|
45
45
|
# annotated with its literal value directly (e.g. ``Literal["red"]``).
|
|
46
46
|
return f"Literal[{self.value_type.get_declaration(self.value)}]"
|
|
@@ -83,7 +83,7 @@ class EnumValue(BaseType):
|
|
|
83
83
|
file_import = FileImport(self.code_model)
|
|
84
84
|
file_import.merge(self.value_type.imports(**kwargs))
|
|
85
85
|
file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB, TypingSection.REGULAR)
|
|
86
|
-
if self.code_model.
|
|
86
|
+
if self.code_model.generate_typeddict_only:
|
|
87
87
|
# In typeddict mode the enums module (``_enums.py``) is never generated
|
|
88
88
|
return file_import
|
|
89
89
|
serialize_namespace = kwargs.get("serialize_namespace", self.code_model.namespace)
|
|
@@ -176,7 +176,7 @@ class EnumType(BaseType):
|
|
|
176
176
|
|
|
177
177
|
@property
|
|
178
178
|
def is_typeddict_mode(self) -> bool:
|
|
179
|
-
return self.code_model.
|
|
179
|
+
return self.code_model.generate_typeddict_only
|
|
180
180
|
|
|
181
181
|
def type_annotation(self, **kwargs: Any) -> str:
|
|
182
182
|
"""The python type used for type annotation
|
|
@@ -184,7 +184,7 @@ class EnumType(BaseType):
|
|
|
184
184
|
:return: The type annotation for this schema
|
|
185
185
|
:rtype: str
|
|
186
186
|
"""
|
|
187
|
-
if self.code_model.options["models-mode"]:
|
|
187
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
188
188
|
if self.is_typeddict_mode:
|
|
189
189
|
# In typeddict mode, enums are Literal aliases defined in types.py
|
|
190
190
|
serialize_namespace_type = kwargs.get("serialize_namespace_type")
|
|
@@ -203,6 +203,10 @@ class EnumType(BaseType):
|
|
|
203
203
|
model_alias = self.code_model.get_unique_models_alias(serialize_namespace, self.client_namespace)
|
|
204
204
|
module_name = f"{model_alias}."
|
|
205
205
|
file_name = f"{self.code_model.enums_filename}." if self.internal else ""
|
|
206
|
+
if serialize_namespace_type == NamespaceType.TYPES_FILE:
|
|
207
|
+
# In types.py the enum symbol is imported directly (bare name), so no ``_enums.``
|
|
208
|
+
# module prefix even for internal enums — the prefix would be an undefined name.
|
|
209
|
+
file_name = ""
|
|
206
210
|
model_name = module_name + file_name + self.name
|
|
207
211
|
# we don't need quoted annotation in operation files, and need it in model folder files.
|
|
208
212
|
if not kwargs.get("is_operation_file", False):
|
|
@@ -221,13 +225,13 @@ class EnumType(BaseType):
|
|
|
221
225
|
return self.value_type.get_declaration(value)
|
|
222
226
|
|
|
223
227
|
def docstring_text(self, **kwargs: Any) -> str:
|
|
224
|
-
if self.code_model.options["models-mode"]:
|
|
228
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
225
229
|
return self.name
|
|
226
230
|
return self.value_type.type_annotation(**kwargs)
|
|
227
231
|
|
|
228
232
|
def docstring_type(self, **kwargs: Any) -> str:
|
|
229
233
|
"""The python type used for RST syntax input and type annotation."""
|
|
230
|
-
if self.code_model.options["models-mode"]:
|
|
234
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
231
235
|
type_annotation = self.value_type.type_annotation(**kwargs)
|
|
232
236
|
enum_type_annotation = f"{self.client_namespace}.models.{self.name}"
|
|
233
237
|
return f"{type_annotation} or ~{enum_type_annotation}"
|
|
@@ -261,7 +265,7 @@ class EnumType(BaseType):
|
|
|
261
265
|
def imports(self, **kwargs: Any) -> FileImport:
|
|
262
266
|
file_import = FileImport(self.code_model)
|
|
263
267
|
file_import.merge(self.value_type.imports(**kwargs))
|
|
264
|
-
if self.code_model.options["models-mode"]:
|
|
268
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
265
269
|
if self.is_typeddict_mode:
|
|
266
270
|
# In typeddict mode, enums are Literal aliases in types.py — no Union needed
|
|
267
271
|
serialize_namespace_type = kwargs.get("serialize_namespace_type")
|
|
@@ -305,9 +309,17 @@ class EnumType(BaseType):
|
|
|
305
309
|
typing_section=TypingSection.REGULAR,
|
|
306
310
|
)
|
|
307
311
|
elif serialize_namespace_type == NamespaceType.TYPES_FILE:
|
|
308
|
-
# Import enum
|
|
312
|
+
# Import the enum symbol directly to avoid dotted forward refs in TypedDict
|
|
313
|
+
# annotations. Internal enums are not re-exported from the public ``models``
|
|
314
|
+
# package, so import them from the private ``_enums`` submodule instead — the
|
|
315
|
+
# bare-symbol import (rather than the ``_enums`` module) also avoids name
|
|
316
|
+
# collisions when internal enums come from several sibling namespaces.
|
|
317
|
+
module_name = f"models.{self.code_model.enums_filename}" if self.internal else "models"
|
|
318
|
+
enums_module = self.code_model.get_relative_import_path(
|
|
319
|
+
serialize_namespace, self.client_namespace, module_name=module_name
|
|
320
|
+
)
|
|
309
321
|
file_import.add_submodule_import(
|
|
310
|
-
|
|
322
|
+
enums_module,
|
|
311
323
|
self.name,
|
|
312
324
|
ImportType.LOCAL,
|
|
313
325
|
typing_section=TypingSection.TYPING,
|
|
@@ -38,6 +38,7 @@ class ListType(BaseType):
|
|
|
38
38
|
self.code_model.options["version-tolerant"]
|
|
39
39
|
and self.element_type.is_xml
|
|
40
40
|
and not self.code_model.options["models-mode"]
|
|
41
|
+
and not self.code_model.generate_typeddict_only
|
|
41
42
|
):
|
|
42
43
|
# this means we're version tolerant XML, we just return the XML element
|
|
43
44
|
return self.element_type.type_annotation(**kwargs)
|
|
@@ -77,9 +77,7 @@ class ModelType(BaseType): # pylint: disable=too-many-instance-attributes, too-
|
|
|
77
77
|
self.cross_language_definition_id: Optional[str] = self.yaml_data.get("crossLanguageDefinitionId")
|
|
78
78
|
self.usage: int = self.yaml_data.get("usage", UsageFlags.Input.value | UsageFlags.Output.value)
|
|
79
79
|
self.client_namespace: str = self.yaml_data.get("clientNamespace", code_model.namespace)
|
|
80
|
-
self.is_typed_dict_only: bool = (
|
|
81
|
-
self.yaml_data.get("typedDictOnly", False) or code_model.options["models-mode"] == "typeddict"
|
|
82
|
-
)
|
|
80
|
+
self.is_typed_dict_only: bool = self.yaml_data.get("typedDictOnly", False) or code_model.generate_typeddict_only
|
|
83
81
|
|
|
84
82
|
@property
|
|
85
83
|
def is_usage_output(self) -> bool:
|
|
@@ -104,6 +104,11 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
104
104
|
else self.has_stream_response
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
+
@property
|
|
108
|
+
def has_structured_stream_response(self) -> bool:
|
|
109
|
+
"""Whether any success response is a structured (JSONL / SSE) stream returning Stream[T]."""
|
|
110
|
+
return any(getattr(r, "is_structured_stream", False) for r in self.responses)
|
|
111
|
+
|
|
107
112
|
@property
|
|
108
113
|
def has_form_data_body(self):
|
|
109
114
|
return self.parameters.has_form_data_body
|
|
@@ -334,7 +339,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
334
339
|
file_import.merge(
|
|
335
340
|
response.imports(async_mode=async_mode, need_import_iobase=self.need_import_iobase, **kwargs)
|
|
336
341
|
)
|
|
337
|
-
if self.code_model.options["models-mode"]:
|
|
342
|
+
if self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only:
|
|
338
343
|
for exception in self.exceptions:
|
|
339
344
|
file_import.merge(exception.imports(async_mode=async_mode, **kwargs))
|
|
340
345
|
|
|
@@ -506,7 +511,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
|
|
|
506
511
|
|
|
507
512
|
@property
|
|
508
513
|
def has_stream_response(self) -> bool:
|
|
509
|
-
return any(r.is_stream_response for r in self.responses)
|
|
514
|
+
return any(r.is_stream_response or r.is_structured_stream for r in self.responses)
|
|
510
515
|
|
|
511
516
|
@classmethod
|
|
512
517
|
def get_request_builder(cls, yaml_data: dict[str, Any], client: "Client"):
|
|
@@ -552,7 +557,12 @@ class Operation(OperationBase[Response]):
|
|
|
552
557
|
"distributed_trace_async",
|
|
553
558
|
ImportType.SDKCORE,
|
|
554
559
|
)
|
|
555
|
-
if
|
|
560
|
+
if (
|
|
561
|
+
self.has_response_body
|
|
562
|
+
and not self.has_optional_return_type
|
|
563
|
+
and not self.code_model.options["models-mode"]
|
|
564
|
+
and not self.code_model.generate_typeddict_only
|
|
565
|
+
):
|
|
556
566
|
file_import.add_submodule_import("typing", "cast", ImportType.STDLIB)
|
|
557
567
|
|
|
558
568
|
return file_import
|