@graphql-codegen/python-operations 0.0.0-stage → 0.0.2-alpha-20261029093428-bb94591c48dc485d04089639f4ca0f124e339666
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 +105 -2
- package/cjs/config.js +0 -0
- package/cjs/index.js +25 -0
- package/cjs/package.json +1 -0
- package/cjs/python-helpers.js +32 -0
- package/cjs/visitor.js +306 -0
- package/esm/config.js +0 -0
- package/esm/index.js +20 -0
- package/esm/python-helpers.js +24 -0
- package/esm/visitor.js +302 -0
- package/package.json +41 -3
- package/typings/config.d.cts +71 -0
- package/typings/config.d.ts +71 -0
- package/typings/index.d.cts +11 -0
- package/typings/index.d.ts +11 -0
- package/typings/python-helpers.d.cts +18 -0
- package/typings/python-helpers.d.ts +18 -0
- package/typings/visitor.d.cts +81 -0
- package/typings/visitor.d.ts +81 -0
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @graphql-codegen/python-operations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A **document-driven** [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) plugin that
|
|
4
|
+
emits **Python operation types**: for each operation in your documents it generates per-operation
|
|
5
|
+
result [pydantic](https://docs.pydantic.dev) `BaseModel` classes (named, nested classes for every
|
|
6
|
+
object selection) plus a `<Operation>Variables` `TypedDict` keyed by the wire (GraphQL) variable
|
|
7
|
+
names. Renamed response keys are pinned with `Field(alias=...)` and a
|
|
8
|
+
`model_config = ConfigDict(populate_by_name=True)`.
|
|
9
|
+
|
|
10
|
+
It is the operations counterpart to `@graphql-codegen/python` (which emits base schema types),
|
|
11
|
+
analogous to how `typescript-operations` complements `typescript`.
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
Schema:
|
|
16
|
+
|
|
17
|
+
```graphql
|
|
18
|
+
enum Role {
|
|
19
|
+
ADMIN
|
|
20
|
+
USER
|
|
21
|
+
}
|
|
22
|
+
type User {
|
|
23
|
+
id: ID!
|
|
24
|
+
name: String!
|
|
25
|
+
role: Role!
|
|
26
|
+
friends: [User!]!
|
|
27
|
+
}
|
|
28
|
+
type Query {
|
|
29
|
+
users(role: Role): [User!]!
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Operation:
|
|
34
|
+
|
|
35
|
+
```graphql
|
|
36
|
+
query GetUsers($role: Role) {
|
|
37
|
+
users(role: $role) {
|
|
38
|
+
id
|
|
39
|
+
name
|
|
40
|
+
role
|
|
41
|
+
friends {
|
|
42
|
+
id
|
|
43
|
+
name
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Output:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from typing import Optional, TypedDict
|
|
53
|
+
from pydantic import BaseModel
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GetUsersQueryUsersFriends(BaseModel):
|
|
57
|
+
id: 'str'
|
|
58
|
+
name: 'str'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GetUsersQueryUsers(BaseModel):
|
|
62
|
+
id: 'str'
|
|
63
|
+
name: 'str'
|
|
64
|
+
role: 'Role'
|
|
65
|
+
friends: 'list[GetUsersQueryUsersFriends]'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GetUsersQuery(BaseModel):
|
|
69
|
+
users: 'list[GetUsersQueryUsers]'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GetUsersQueryVariables(TypedDict):
|
|
73
|
+
role: 'Optional[Role]'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Result types are pydantic `BaseModel` subclasses; the `<Operation>Variables` type stays a
|
|
77
|
+
`TypedDict`. When a response key is renamed (snake_case or a reserved keyword), that attribute is
|
|
78
|
+
pinned with `Field(alias='<wireName>')` and the class gets
|
|
79
|
+
`model_config = ConfigDict(populate_by_name=True)`.
|
|
80
|
+
|
|
81
|
+
## Config
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Description |
|
|
84
|
+
| ------------------ | ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `namingConvention` | `'keep' \| 'snakeCase'` | `'snakeCase'` | How response keys become result-class attributes. `snakeCase` converts camelCase→snake*case, suffixes reserved keywords with `*`, and pins renamed keys via `Field(alias=...)`. `keep` uses keys verbatim. Variables always keep the wire name. |
|
|
86
|
+
| `baseModel` | `string` | `'BaseModel'` | Base class the generated result types extend. |
|
|
87
|
+
| `runtimeImport` | `string` | `'pydantic'` | Module the base model is imported from (`from {runtimeImport} import {baseModel}`, with `ConfigDict`/`Field` appended when aliases are used). |
|
|
88
|
+
| `typesImport` | `string` | – | If set, prepend `from {typesImport} import *` so base type names (enums, inputs) resolve at runtime. |
|
|
89
|
+
|
|
90
|
+
## Behaviour / naming
|
|
91
|
+
|
|
92
|
+
- Result root type: `<OperationName><OperationType>` (e.g. `GetUsersQuery`).
|
|
93
|
+
- Nested object selections: `<ParentTypeName><PascalCase(responseFieldName)>`.
|
|
94
|
+
- Variables type: `<OperationName><OperationType>Variables`.
|
|
95
|
+
- Field names are `snake_case`; Python reserved keywords get a trailing `_`.
|
|
96
|
+
- Aliases use the alias as the response key; `__typename` becomes `__typename__`.
|
|
97
|
+
- Every annotation is quoted (forward reference), so emit order does not matter.
|
|
98
|
+
|
|
99
|
+
## Not (yet) supported
|
|
100
|
+
|
|
101
|
+
Fragment spreads, inline fragments and union member (`... on Type`) selections are not yet
|
|
102
|
+
implemented. Rather than silently omitting their fields (which would produce a result type that
|
|
103
|
+
looks valid but is missing data), the plugin **throws a clear error** at generate time if an
|
|
104
|
+
operation contains one — inline the fields directly into the operation for now. See
|
|
105
|
+
`src/python-helpers.ts` for the duplicated naming helpers (TODO: extract to a shared
|
|
106
|
+
`@graphql-codegen/python-common` package).
|
package/cjs/config.js
ADDED
|
File without changes
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.plugin = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const graphql_1 = require("graphql");
|
|
6
|
+
const visitor_js_1 = require("./visitor.js");
|
|
7
|
+
tslib_1.__exportStar(require("./config.js"), exports);
|
|
8
|
+
tslib_1.__exportStar(require("./visitor.js"), exports);
|
|
9
|
+
tslib_1.__exportStar(require("./python-helpers.js"), exports);
|
|
10
|
+
/**
|
|
11
|
+
* Document-driven plugin: for each GraphQL operation in the provided documents,
|
|
12
|
+
* emit a per-operation result `TypedDict` (with named nested `TypedDict`s for
|
|
13
|
+
* every object selection) plus a `<Operation>Variables` `TypedDict`.
|
|
14
|
+
*/
|
|
15
|
+
const plugin = (schema, documents, config) => {
|
|
16
|
+
const allAst = (0, graphql_1.concatAST)(documents.map(d => d.document).filter(Boolean));
|
|
17
|
+
const operations = allAst.definitions.filter((def) => def.kind === graphql_1.Kind.OPERATION_DEFINITION);
|
|
18
|
+
const visitor = new visitor_js_1.PythonOperationsVisitor(schema, config);
|
|
19
|
+
const definitions = operations.map(op => visitor.generateOperation(op));
|
|
20
|
+
return {
|
|
21
|
+
prepend: visitor.getImports(),
|
|
22
|
+
content: definitions.join('\n\n\n'),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
exports.plugin = plugin;
|
package/cjs/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pyFieldName = exports.PYTHON_SCALARS = exports.isValidClassAttr = exports.isReservedField = void 0;
|
|
4
|
+
exports.resultFieldKey = resultFieldKey;
|
|
5
|
+
exports.fieldKey = fieldKey;
|
|
6
|
+
const python_common_1 = require("@graphql-codegen/python-common");
|
|
7
|
+
Object.defineProperty(exports, "isReservedField", { enumerable: true, get: function () { return python_common_1.isReservedField; } });
|
|
8
|
+
Object.defineProperty(exports, "isValidClassAttr", { enumerable: true, get: function () { return python_common_1.isValidClassAttr; } });
|
|
9
|
+
Object.defineProperty(exports, "pyFieldName", { enumerable: true, get: function () { return python_common_1.pythonFieldName; } });
|
|
10
|
+
Object.defineProperty(exports, "PYTHON_SCALARS", { enumerable: true, get: function () { return python_common_1.PYTHON_SCALARS; } });
|
|
11
|
+
/**
|
|
12
|
+
* Compute the attribute name for a `BaseModel` result class. Under `'snakeCase'`
|
|
13
|
+
* the key is snake-cased (keywords suffixed with `_`); under `'keep'` the wire
|
|
14
|
+
* name is used verbatim, but — because `BaseModel` classes cannot use functional
|
|
15
|
+
* `TypedDict` syntax — a keyword / non-identifier name still falls back to a
|
|
16
|
+
* trailing `_`.
|
|
17
|
+
*/
|
|
18
|
+
function resultFieldKey(name, convention) {
|
|
19
|
+
if (convention === 'snakeCase') {
|
|
20
|
+
return (0, python_common_1.pythonFieldName)(name);
|
|
21
|
+
}
|
|
22
|
+
return (0, python_common_1.isValidClassAttr)(name) ? name : `${name}_`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compute the `TypedDict` key for a GraphQL response / variable name under the
|
|
26
|
+
* given naming convention. Under `'keep'` the raw name is returned verbatim
|
|
27
|
+
* (functional `TypedDict` syntax handles invalid keys); under `'snakeCase'` the
|
|
28
|
+
* legacy snake_case + `_`-suffix behavior is applied.
|
|
29
|
+
*/
|
|
30
|
+
function fieldKey(name, convention) {
|
|
31
|
+
return convention === 'snakeCase' ? (0, python_common_1.pythonFieldName)(name) : name;
|
|
32
|
+
}
|
package/cjs/visitor.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PythonOperationsVisitor = void 0;
|
|
4
|
+
const graphql_1 = require("graphql");
|
|
5
|
+
const visitor_plugin_common_1 = require("@graphql-codegen/visitor-plugin-common");
|
|
6
|
+
const python_helpers_js_1 = require("./python-helpers.js");
|
|
7
|
+
const INDENT = ' ';
|
|
8
|
+
/**
|
|
9
|
+
* Walks GraphQL `OperationDefinition` nodes against the schema and recursively
|
|
10
|
+
* emits named, nested Python `TypedDict` classes for every object selection set,
|
|
11
|
+
* plus a `<Operation>Variables` `TypedDict` for the operation variables.
|
|
12
|
+
*
|
|
13
|
+
* Unlike the C-family `SelectionSetToObject`/`DeclarationBlock` approach (which
|
|
14
|
+
* emits inline anonymous objects), this mirrors the C#/Java strategy: every
|
|
15
|
+
* nested selection becomes its own named type referenced by its parent.
|
|
16
|
+
*/
|
|
17
|
+
class PythonOperationsVisitor extends visitor_plugin_common_1.BaseVisitor {
|
|
18
|
+
constructor(schema, rawConfig) {
|
|
19
|
+
// Our `namingConvention` (`'keep'` | `'snakeCase'`) governs field-KEY naming
|
|
20
|
+
// only. It must NOT leak into the base visitor's `namingConvention`, which
|
|
21
|
+
// drives TYPE-name conversion (`convertName` / `pascal`) — those names must
|
|
22
|
+
// stay PascalCase (e.g. `GetUsersQueryUsers`).
|
|
23
|
+
super({ ...rawConfig, namingConvention: undefined }, {});
|
|
24
|
+
this._usesOptional = false;
|
|
25
|
+
this._usesAny = false;
|
|
26
|
+
this._usesLiteral = false;
|
|
27
|
+
this._usesBaseModel = false;
|
|
28
|
+
this._usesConfigDict = false;
|
|
29
|
+
this._usesField = false;
|
|
30
|
+
this._anonymousCount = 0;
|
|
31
|
+
this._schema = schema;
|
|
32
|
+
this._typesImport = rawConfig.typesImport;
|
|
33
|
+
this._namingConvention = (0, visitor_plugin_common_1.getConfigValue)(rawConfig.namingConvention, 'snakeCase');
|
|
34
|
+
this._baseModel = (0, visitor_plugin_common_1.getConfigValue)(rawConfig.baseModel, 'BaseModel');
|
|
35
|
+
this._runtimeImport = (0, visitor_plugin_common_1.getConfigValue)(rawConfig.runtimeImport, 'pydantic');
|
|
36
|
+
}
|
|
37
|
+
/** PascalCase a name segment without applying any prefix/suffix. */
|
|
38
|
+
pascal(name) {
|
|
39
|
+
return this.convertName(name, {
|
|
40
|
+
useTypesPrefix: false,
|
|
41
|
+
useTypesSuffix: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
get operationRootTypes() {
|
|
45
|
+
return {
|
|
46
|
+
query: this._schema.getQueryType(),
|
|
47
|
+
mutation: this._schema.getMutationType(),
|
|
48
|
+
subscription: this._schema.getSubscriptionType(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// --- Python annotation builders ----------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Build a Python annotation from a (resolved) GraphQL output type. The
|
|
54
|
+
* `objectBaseName` is the generated nested type name used when the base type
|
|
55
|
+
* is an object/interface/union selection.
|
|
56
|
+
*/
|
|
57
|
+
pyTypeFromGraphQL(type, objectBaseName) {
|
|
58
|
+
if ((0, graphql_1.isNonNullType)(type)) {
|
|
59
|
+
return this.innerFromGraphQL(type.ofType, objectBaseName);
|
|
60
|
+
}
|
|
61
|
+
this._usesOptional = true;
|
|
62
|
+
return `Optional[${this.innerFromGraphQL(type, objectBaseName)}]`;
|
|
63
|
+
}
|
|
64
|
+
innerFromGraphQL(type, objectBaseName) {
|
|
65
|
+
if ((0, graphql_1.isListType)(type)) {
|
|
66
|
+
return `list[${this.pyTypeFromGraphQL(type.ofType, objectBaseName)}]`;
|
|
67
|
+
}
|
|
68
|
+
const named = (0, graphql_1.getNamedType)(type);
|
|
69
|
+
if ((0, graphql_1.isScalarType)(named)) {
|
|
70
|
+
const mapped = python_helpers_js_1.PYTHON_SCALARS[named.name];
|
|
71
|
+
if (mapped) {
|
|
72
|
+
return mapped.output;
|
|
73
|
+
}
|
|
74
|
+
this._usesAny = true;
|
|
75
|
+
return 'Any';
|
|
76
|
+
}
|
|
77
|
+
if ((0, graphql_1.isEnumType)(named)) {
|
|
78
|
+
return named.name;
|
|
79
|
+
}
|
|
80
|
+
// object / interface / union selection -> generated nested type name
|
|
81
|
+
return objectBaseName;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a Python annotation from a variable `TypeNode`. Named types resolve
|
|
85
|
+
* to mapped scalars, or the bare base type name (enums / input objects).
|
|
86
|
+
*/
|
|
87
|
+
pyTypeFromTypeNode(type) {
|
|
88
|
+
if (type.kind === graphql_1.Kind.NON_NULL_TYPE) {
|
|
89
|
+
return this.innerFromTypeNode(type.type);
|
|
90
|
+
}
|
|
91
|
+
this._usesOptional = true;
|
|
92
|
+
return `Optional[${this.innerFromTypeNode(type)}]`;
|
|
93
|
+
}
|
|
94
|
+
innerFromTypeNode(type) {
|
|
95
|
+
if (type.kind === graphql_1.Kind.LIST_TYPE) {
|
|
96
|
+
return `list[${this.pyTypeFromTypeNode(type.type)}]`;
|
|
97
|
+
}
|
|
98
|
+
if (type.kind === graphql_1.Kind.NON_NULL_TYPE) {
|
|
99
|
+
// Non-null base (e.g. inside a list) — no Optional wrapper.
|
|
100
|
+
return this.innerFromTypeNode(type.type);
|
|
101
|
+
}
|
|
102
|
+
// NamedType
|
|
103
|
+
const name = type.name.value;
|
|
104
|
+
const mapped = python_helpers_js_1.PYTHON_SCALARS[name];
|
|
105
|
+
if (mapped) {
|
|
106
|
+
return mapped.input;
|
|
107
|
+
}
|
|
108
|
+
const schemaType = this._schema.getType(name);
|
|
109
|
+
if (schemaType && (0, graphql_1.isScalarType)(schemaType)) {
|
|
110
|
+
this._usesAny = true;
|
|
111
|
+
return 'Any';
|
|
112
|
+
}
|
|
113
|
+
// enum or input object -> bare name (quoted forward ref by caller)
|
|
114
|
+
return name;
|
|
115
|
+
}
|
|
116
|
+
// --- Class builders ----------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* Build a variables `TypedDict`. Variable keys are ALWAYS the wire (GraphQL)
|
|
119
|
+
* name — they are sent to the server as-is. If every key is a valid Python
|
|
120
|
+
* attribute the readable class syntax is used; otherwise (a key is a keyword /
|
|
121
|
+
* non-identifier) the functional `TypedDict('Name', { 'key': '...' })` syntax
|
|
122
|
+
* is emitted so the offending keys remain valid.
|
|
123
|
+
*/
|
|
124
|
+
buildVariablesClass(name, fields) {
|
|
125
|
+
if (fields.length === 0) {
|
|
126
|
+
return `class ${name}(TypedDict):\n${INDENT}pass`;
|
|
127
|
+
}
|
|
128
|
+
const needsFunctional = fields.some(f => !(0, python_helpers_js_1.isValidClassAttr)(f.key));
|
|
129
|
+
if (needsFunctional) {
|
|
130
|
+
const entries = fields.map(f => `${INDENT}'${f.key}': '${f.annotation}',`);
|
|
131
|
+
return [`${name} = TypedDict('${name}', {`, ...entries, '})'].join('\n');
|
|
132
|
+
}
|
|
133
|
+
const lines = fields.map(f => `${INDENT}${f.key}: '${f.annotation}'`);
|
|
134
|
+
return [`class ${name}(TypedDict):`, ...lines].join('\n');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Emit the per-class `__init__` that pre-maps wire keys to their escaped
|
|
138
|
+
* attribute names before delegating to the base model. Only emitted for result
|
|
139
|
+
* classes that contain keyword fields.
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* Render a single result field line: `key: 'annotation'`, with a pydantic
|
|
143
|
+
* `Field(alias=...)` when the Python attribute differs from the wire response
|
|
144
|
+
* key, and a `None` default for optional fields.
|
|
145
|
+
*/
|
|
146
|
+
renderResultField(field) {
|
|
147
|
+
const optional = field.annotation.startsWith('Optional[');
|
|
148
|
+
let assignment = '';
|
|
149
|
+
if (field.wire !== undefined && field.key !== field.wire) {
|
|
150
|
+
this._usesField = true;
|
|
151
|
+
assignment = optional
|
|
152
|
+
? ` = Field(default=None, alias='${field.wire}')`
|
|
153
|
+
: ` = Field(alias='${field.wire}')`;
|
|
154
|
+
}
|
|
155
|
+
else if (optional) {
|
|
156
|
+
assignment = ' = None';
|
|
157
|
+
}
|
|
158
|
+
return `${field.key}: '${field.annotation}'${assignment}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Build a result base-model subclass. A field whose Python attribute differs
|
|
162
|
+
* from the wire key is pinned with `Field(alias=...)`, and a
|
|
163
|
+
* `model_config = ConfigDict(populate_by_name=True)` line is prepended so such
|
|
164
|
+
* classes can also be built by attribute name.
|
|
165
|
+
*/
|
|
166
|
+
buildResultClass(name, fields) {
|
|
167
|
+
this._usesBaseModel = true;
|
|
168
|
+
const header = `class ${name}(${this._baseModel}):`;
|
|
169
|
+
if (fields.length === 0) {
|
|
170
|
+
return `${header}\n${INDENT}pass`;
|
|
171
|
+
}
|
|
172
|
+
const lines = [header];
|
|
173
|
+
if (fields.some(f => f.wire !== undefined && f.key !== f.wire)) {
|
|
174
|
+
this._usesConfigDict = true;
|
|
175
|
+
lines.push(`${INDENT}model_config = ConfigDict(populate_by_name=True)`);
|
|
176
|
+
}
|
|
177
|
+
for (const field of fields) {
|
|
178
|
+
lines.push(`${INDENT}${this.renderResultField(field)}`);
|
|
179
|
+
}
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
// --- Selection set walking ---------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Emit a `TypedDict` for `selectionSet` (a selection against `parentType`),
|
|
185
|
+
* named `typeName`. Nested object selections are emitted FIRST (deepest
|
|
186
|
+
* first) so references resolve top-to-bottom. Returns nothing; classes are
|
|
187
|
+
* pushed onto `out`.
|
|
188
|
+
*/
|
|
189
|
+
processSelectionSet(parentType, selectionSet, typeName, out) {
|
|
190
|
+
var _a, _b, _c, _d;
|
|
191
|
+
const fields = [];
|
|
192
|
+
const addField = (responseKey, annotation) => {
|
|
193
|
+
const key = (0, python_helpers_js_1.resultFieldKey)(responseKey, this._namingConvention);
|
|
194
|
+
// Record the wire (response) key so a `Field(alias=...)` is emitted when
|
|
195
|
+
// the Python attribute differs (snake_case conversion or keyword escape).
|
|
196
|
+
fields.push({ key, annotation, wire: responseKey });
|
|
197
|
+
};
|
|
198
|
+
for (const selection of selectionSet.selections) {
|
|
199
|
+
// Fragments (spread / inline) are not supported yet. Fail loudly instead
|
|
200
|
+
// of silently omitting their fields, which would otherwise produce a
|
|
201
|
+
// result type that looks valid but is missing data.
|
|
202
|
+
if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) {
|
|
203
|
+
throw new Error(`@graphql-codegen/python-operations: fragment spread "...${selection.name.value}" on ` +
|
|
204
|
+
`"${parentType.name}" is not supported yet. Inline the fragment's fields directly into ` +
|
|
205
|
+
'the operation for now.');
|
|
206
|
+
}
|
|
207
|
+
if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT) {
|
|
208
|
+
const onType = (_b = (_a = selection.typeCondition) === null || _a === void 0 ? void 0 : _a.name.value) !== null && _b !== void 0 ? _b : parentType.name;
|
|
209
|
+
throw new Error(`@graphql-codegen/python-operations: inline fragment "... on ${onType}" on ` +
|
|
210
|
+
`"${parentType.name}" is not supported yet. Union/interface member selections cannot ` +
|
|
211
|
+
'be generated; select only common fields for now.');
|
|
212
|
+
}
|
|
213
|
+
const fieldNode = selection;
|
|
214
|
+
const fieldName = fieldNode.name.value;
|
|
215
|
+
const responseKey = (_d = (_c = fieldNode.alias) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : fieldName;
|
|
216
|
+
if (fieldName === '__typename') {
|
|
217
|
+
// `__typename` selections become a `Literal["ParentType"]` attribute.
|
|
218
|
+
// pydantic forbids a leading-underscore field name, so the attribute is
|
|
219
|
+
// `typename__` with `Field(alias='__typename')` pinning the wire key.
|
|
220
|
+
this._usesLiteral = true;
|
|
221
|
+
fields.push({
|
|
222
|
+
key: 'typename__',
|
|
223
|
+
annotation: `Literal["${parentType.name}"]`,
|
|
224
|
+
wire: '__typename',
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const fieldDef = parentType.getFields()[fieldName];
|
|
229
|
+
if (!fieldDef) {
|
|
230
|
+
// Unknown field (e.g. introspection on a type without it) — skip.
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const fieldType = fieldDef.type;
|
|
234
|
+
const namedType = (0, graphql_1.getNamedType)(fieldType);
|
|
235
|
+
if (((0, graphql_1.isObjectType)(namedType) || (0, graphql_1.isInterfaceType)(namedType)) && fieldNode.selectionSet) {
|
|
236
|
+
const nestedName = `${typeName}${this.pascal(responseKey)}`;
|
|
237
|
+
// Recurse FIRST so deepest types are emitted before this one.
|
|
238
|
+
this.processSelectionSet(namedType, fieldNode.selectionSet, nestedName, out);
|
|
239
|
+
addField(responseKey, this.pyTypeFromGraphQL(fieldType, nestedName));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Scalar / enum leaf (or union without object handling).
|
|
243
|
+
addField(responseKey, this.pyTypeFromGraphQL(fieldType, namedType.name));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
out.push(this.buildResultClass(typeName, fields));
|
|
247
|
+
}
|
|
248
|
+
buildVariables(variableDefinitions, variablesTypeName) {
|
|
249
|
+
const fields = variableDefinitions.map(def => {
|
|
250
|
+
// Variable keys are ALWAYS the wire name (sent to the server as-is).
|
|
251
|
+
const name = def.variable.name.value;
|
|
252
|
+
const annotation = this.pyTypeFromTypeNode(def.type);
|
|
253
|
+
return { key: name, annotation };
|
|
254
|
+
});
|
|
255
|
+
return this.buildVariablesClass(variablesTypeName, fields);
|
|
256
|
+
}
|
|
257
|
+
/** Generate all types for a single operation. Returns joined class blocks. */
|
|
258
|
+
generateOperation(operation) {
|
|
259
|
+
var _a, _b, _c;
|
|
260
|
+
const operationType = operation.operation; // 'query' | 'mutation' | 'subscription'
|
|
261
|
+
const operationTypeName = operationType.charAt(0).toUpperCase() + operationType.slice(1);
|
|
262
|
+
const operationName = (_b = (_a = operation.name) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : `Anonymous${this._anonymousCount++}`;
|
|
263
|
+
const baseName = `${this.pascal(operationName)}${operationTypeName}`;
|
|
264
|
+
const rootType = this.operationRootTypes[operationType];
|
|
265
|
+
const out = [];
|
|
266
|
+
if (rootType) {
|
|
267
|
+
this.processSelectionSet(rootType, operation.selectionSet, baseName, out);
|
|
268
|
+
}
|
|
269
|
+
out.push(this.buildVariables((_c = operation.variableDefinitions) !== null && _c !== void 0 ? _c : [], `${baseName}Variables`));
|
|
270
|
+
return out.join('\n\n\n');
|
|
271
|
+
}
|
|
272
|
+
/** Build the `prepend` import lines, emitting only what is referenced. */
|
|
273
|
+
getImports() {
|
|
274
|
+
const imports = [];
|
|
275
|
+
const typingParts = [];
|
|
276
|
+
if (this._usesOptional) {
|
|
277
|
+
typingParts.push('Optional');
|
|
278
|
+
}
|
|
279
|
+
// Every operation emits at least a variables TypedDict.
|
|
280
|
+
typingParts.push('TypedDict');
|
|
281
|
+
if (this._usesLiteral) {
|
|
282
|
+
typingParts.push('Literal');
|
|
283
|
+
}
|
|
284
|
+
if (this._usesAny) {
|
|
285
|
+
typingParts.push('Any');
|
|
286
|
+
}
|
|
287
|
+
imports.push(`from typing import ${typingParts.join(', ')}`);
|
|
288
|
+
if (this._usesBaseModel) {
|
|
289
|
+
// `ConfigDict` / `Field` are pydantic names; emit them from the same module
|
|
290
|
+
// as the base model, and only when the generated content uses them.
|
|
291
|
+
const parts = [this._baseModel];
|
|
292
|
+
if (this._usesConfigDict) {
|
|
293
|
+
parts.push('ConfigDict');
|
|
294
|
+
}
|
|
295
|
+
if (this._usesField) {
|
|
296
|
+
parts.push('Field');
|
|
297
|
+
}
|
|
298
|
+
imports.push(`from ${this._runtimeImport} import ${parts.join(', ')}`);
|
|
299
|
+
}
|
|
300
|
+
if (this._typesImport) {
|
|
301
|
+
imports.push(`from ${this._typesImport} import *`);
|
|
302
|
+
}
|
|
303
|
+
return imports;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
exports.PythonOperationsVisitor = PythonOperationsVisitor;
|
package/esm/config.js
ADDED
|
File without changes
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { concatAST, Kind } from 'graphql';
|
|
2
|
+
import { PythonOperationsVisitor } from './visitor.js';
|
|
3
|
+
export * from './config.js';
|
|
4
|
+
export * from './visitor.js';
|
|
5
|
+
export * from './python-helpers.js';
|
|
6
|
+
/**
|
|
7
|
+
* Document-driven plugin: for each GraphQL operation in the provided documents,
|
|
8
|
+
* emit a per-operation result `TypedDict` (with named nested `TypedDict`s for
|
|
9
|
+
* every object selection) plus a `<Operation>Variables` `TypedDict`.
|
|
10
|
+
*/
|
|
11
|
+
export const plugin = (schema, documents, config) => {
|
|
12
|
+
const allAst = concatAST(documents.map(d => d.document).filter(Boolean));
|
|
13
|
+
const operations = allAst.definitions.filter((def) => def.kind === Kind.OPERATION_DEFINITION);
|
|
14
|
+
const visitor = new PythonOperationsVisitor(schema, config);
|
|
15
|
+
const definitions = operations.map(op => visitor.generateOperation(op));
|
|
16
|
+
return {
|
|
17
|
+
prepend: visitor.getImports(),
|
|
18
|
+
content: definitions.join('\n\n\n'),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isReservedField, isValidClassAttr, pythonFieldName as pyFieldName, PYTHON_SCALARS, } from '@graphql-codegen/python-common';
|
|
2
|
+
export { isReservedField, isValidClassAttr, PYTHON_SCALARS, pyFieldName };
|
|
3
|
+
/**
|
|
4
|
+
* Compute the attribute name for a `BaseModel` result class. Under `'snakeCase'`
|
|
5
|
+
* the key is snake-cased (keywords suffixed with `_`); under `'keep'` the wire
|
|
6
|
+
* name is used verbatim, but — because `BaseModel` classes cannot use functional
|
|
7
|
+
* `TypedDict` syntax — a keyword / non-identifier name still falls back to a
|
|
8
|
+
* trailing `_`.
|
|
9
|
+
*/
|
|
10
|
+
export function resultFieldKey(name, convention) {
|
|
11
|
+
if (convention === 'snakeCase') {
|
|
12
|
+
return pyFieldName(name);
|
|
13
|
+
}
|
|
14
|
+
return isValidClassAttr(name) ? name : `${name}_`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compute the `TypedDict` key for a GraphQL response / variable name under the
|
|
18
|
+
* given naming convention. Under `'keep'` the raw name is returned verbatim
|
|
19
|
+
* (functional `TypedDict` syntax handles invalid keys); under `'snakeCase'` the
|
|
20
|
+
* legacy snake_case + `_`-suffix behavior is applied.
|
|
21
|
+
*/
|
|
22
|
+
export function fieldKey(name, convention) {
|
|
23
|
+
return convention === 'snakeCase' ? pyFieldName(name) : name;
|
|
24
|
+
}
|
package/esm/visitor.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { getNamedType, isEnumType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType, Kind, } from 'graphql';
|
|
2
|
+
import { BaseVisitor, getConfigValue } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
+
import { isValidClassAttr, PYTHON_SCALARS, resultFieldKey, } from './python-helpers.js';
|
|
4
|
+
const INDENT = ' ';
|
|
5
|
+
/**
|
|
6
|
+
* Walks GraphQL `OperationDefinition` nodes against the schema and recursively
|
|
7
|
+
* emits named, nested Python `TypedDict` classes for every object selection set,
|
|
8
|
+
* plus a `<Operation>Variables` `TypedDict` for the operation variables.
|
|
9
|
+
*
|
|
10
|
+
* Unlike the C-family `SelectionSetToObject`/`DeclarationBlock` approach (which
|
|
11
|
+
* emits inline anonymous objects), this mirrors the C#/Java strategy: every
|
|
12
|
+
* nested selection becomes its own named type referenced by its parent.
|
|
13
|
+
*/
|
|
14
|
+
export class PythonOperationsVisitor extends BaseVisitor {
|
|
15
|
+
constructor(schema, rawConfig) {
|
|
16
|
+
// Our `namingConvention` (`'keep'` | `'snakeCase'`) governs field-KEY naming
|
|
17
|
+
// only. It must NOT leak into the base visitor's `namingConvention`, which
|
|
18
|
+
// drives TYPE-name conversion (`convertName` / `pascal`) — those names must
|
|
19
|
+
// stay PascalCase (e.g. `GetUsersQueryUsers`).
|
|
20
|
+
super({ ...rawConfig, namingConvention: undefined }, {});
|
|
21
|
+
this._usesOptional = false;
|
|
22
|
+
this._usesAny = false;
|
|
23
|
+
this._usesLiteral = false;
|
|
24
|
+
this._usesBaseModel = false;
|
|
25
|
+
this._usesConfigDict = false;
|
|
26
|
+
this._usesField = false;
|
|
27
|
+
this._anonymousCount = 0;
|
|
28
|
+
this._schema = schema;
|
|
29
|
+
this._typesImport = rawConfig.typesImport;
|
|
30
|
+
this._namingConvention = getConfigValue(rawConfig.namingConvention, 'snakeCase');
|
|
31
|
+
this._baseModel = getConfigValue(rawConfig.baseModel, 'BaseModel');
|
|
32
|
+
this._runtimeImport = getConfigValue(rawConfig.runtimeImport, 'pydantic');
|
|
33
|
+
}
|
|
34
|
+
/** PascalCase a name segment without applying any prefix/suffix. */
|
|
35
|
+
pascal(name) {
|
|
36
|
+
return this.convertName(name, {
|
|
37
|
+
useTypesPrefix: false,
|
|
38
|
+
useTypesSuffix: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
get operationRootTypes() {
|
|
42
|
+
return {
|
|
43
|
+
query: this._schema.getQueryType(),
|
|
44
|
+
mutation: this._schema.getMutationType(),
|
|
45
|
+
subscription: this._schema.getSubscriptionType(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// --- Python annotation builders ----------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Build a Python annotation from a (resolved) GraphQL output type. The
|
|
51
|
+
* `objectBaseName` is the generated nested type name used when the base type
|
|
52
|
+
* is an object/interface/union selection.
|
|
53
|
+
*/
|
|
54
|
+
pyTypeFromGraphQL(type, objectBaseName) {
|
|
55
|
+
if (isNonNullType(type)) {
|
|
56
|
+
return this.innerFromGraphQL(type.ofType, objectBaseName);
|
|
57
|
+
}
|
|
58
|
+
this._usesOptional = true;
|
|
59
|
+
return `Optional[${this.innerFromGraphQL(type, objectBaseName)}]`;
|
|
60
|
+
}
|
|
61
|
+
innerFromGraphQL(type, objectBaseName) {
|
|
62
|
+
if (isListType(type)) {
|
|
63
|
+
return `list[${this.pyTypeFromGraphQL(type.ofType, objectBaseName)}]`;
|
|
64
|
+
}
|
|
65
|
+
const named = getNamedType(type);
|
|
66
|
+
if (isScalarType(named)) {
|
|
67
|
+
const mapped = PYTHON_SCALARS[named.name];
|
|
68
|
+
if (mapped) {
|
|
69
|
+
return mapped.output;
|
|
70
|
+
}
|
|
71
|
+
this._usesAny = true;
|
|
72
|
+
return 'Any';
|
|
73
|
+
}
|
|
74
|
+
if (isEnumType(named)) {
|
|
75
|
+
return named.name;
|
|
76
|
+
}
|
|
77
|
+
// object / interface / union selection -> generated nested type name
|
|
78
|
+
return objectBaseName;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a Python annotation from a variable `TypeNode`. Named types resolve
|
|
82
|
+
* to mapped scalars, or the bare base type name (enums / input objects).
|
|
83
|
+
*/
|
|
84
|
+
pyTypeFromTypeNode(type) {
|
|
85
|
+
if (type.kind === Kind.NON_NULL_TYPE) {
|
|
86
|
+
return this.innerFromTypeNode(type.type);
|
|
87
|
+
}
|
|
88
|
+
this._usesOptional = true;
|
|
89
|
+
return `Optional[${this.innerFromTypeNode(type)}]`;
|
|
90
|
+
}
|
|
91
|
+
innerFromTypeNode(type) {
|
|
92
|
+
if (type.kind === Kind.LIST_TYPE) {
|
|
93
|
+
return `list[${this.pyTypeFromTypeNode(type.type)}]`;
|
|
94
|
+
}
|
|
95
|
+
if (type.kind === Kind.NON_NULL_TYPE) {
|
|
96
|
+
// Non-null base (e.g. inside a list) — no Optional wrapper.
|
|
97
|
+
return this.innerFromTypeNode(type.type);
|
|
98
|
+
}
|
|
99
|
+
// NamedType
|
|
100
|
+
const name = type.name.value;
|
|
101
|
+
const mapped = PYTHON_SCALARS[name];
|
|
102
|
+
if (mapped) {
|
|
103
|
+
return mapped.input;
|
|
104
|
+
}
|
|
105
|
+
const schemaType = this._schema.getType(name);
|
|
106
|
+
if (schemaType && isScalarType(schemaType)) {
|
|
107
|
+
this._usesAny = true;
|
|
108
|
+
return 'Any';
|
|
109
|
+
}
|
|
110
|
+
// enum or input object -> bare name (quoted forward ref by caller)
|
|
111
|
+
return name;
|
|
112
|
+
}
|
|
113
|
+
// --- Class builders ----------------------------------------------------
|
|
114
|
+
/**
|
|
115
|
+
* Build a variables `TypedDict`. Variable keys are ALWAYS the wire (GraphQL)
|
|
116
|
+
* name — they are sent to the server as-is. If every key is a valid Python
|
|
117
|
+
* attribute the readable class syntax is used; otherwise (a key is a keyword /
|
|
118
|
+
* non-identifier) the functional `TypedDict('Name', { 'key': '...' })` syntax
|
|
119
|
+
* is emitted so the offending keys remain valid.
|
|
120
|
+
*/
|
|
121
|
+
buildVariablesClass(name, fields) {
|
|
122
|
+
if (fields.length === 0) {
|
|
123
|
+
return `class ${name}(TypedDict):\n${INDENT}pass`;
|
|
124
|
+
}
|
|
125
|
+
const needsFunctional = fields.some(f => !isValidClassAttr(f.key));
|
|
126
|
+
if (needsFunctional) {
|
|
127
|
+
const entries = fields.map(f => `${INDENT}'${f.key}': '${f.annotation}',`);
|
|
128
|
+
return [`${name} = TypedDict('${name}', {`, ...entries, '})'].join('\n');
|
|
129
|
+
}
|
|
130
|
+
const lines = fields.map(f => `${INDENT}${f.key}: '${f.annotation}'`);
|
|
131
|
+
return [`class ${name}(TypedDict):`, ...lines].join('\n');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Emit the per-class `__init__` that pre-maps wire keys to their escaped
|
|
135
|
+
* attribute names before delegating to the base model. Only emitted for result
|
|
136
|
+
* classes that contain keyword fields.
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Render a single result field line: `key: 'annotation'`, with a pydantic
|
|
140
|
+
* `Field(alias=...)` when the Python attribute differs from the wire response
|
|
141
|
+
* key, and a `None` default for optional fields.
|
|
142
|
+
*/
|
|
143
|
+
renderResultField(field) {
|
|
144
|
+
const optional = field.annotation.startsWith('Optional[');
|
|
145
|
+
let assignment = '';
|
|
146
|
+
if (field.wire !== undefined && field.key !== field.wire) {
|
|
147
|
+
this._usesField = true;
|
|
148
|
+
assignment = optional
|
|
149
|
+
? ` = Field(default=None, alias='${field.wire}')`
|
|
150
|
+
: ` = Field(alias='${field.wire}')`;
|
|
151
|
+
}
|
|
152
|
+
else if (optional) {
|
|
153
|
+
assignment = ' = None';
|
|
154
|
+
}
|
|
155
|
+
return `${field.key}: '${field.annotation}'${assignment}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build a result base-model subclass. A field whose Python attribute differs
|
|
159
|
+
* from the wire key is pinned with `Field(alias=...)`, and a
|
|
160
|
+
* `model_config = ConfigDict(populate_by_name=True)` line is prepended so such
|
|
161
|
+
* classes can also be built by attribute name.
|
|
162
|
+
*/
|
|
163
|
+
buildResultClass(name, fields) {
|
|
164
|
+
this._usesBaseModel = true;
|
|
165
|
+
const header = `class ${name}(${this._baseModel}):`;
|
|
166
|
+
if (fields.length === 0) {
|
|
167
|
+
return `${header}\n${INDENT}pass`;
|
|
168
|
+
}
|
|
169
|
+
const lines = [header];
|
|
170
|
+
if (fields.some(f => f.wire !== undefined && f.key !== f.wire)) {
|
|
171
|
+
this._usesConfigDict = true;
|
|
172
|
+
lines.push(`${INDENT}model_config = ConfigDict(populate_by_name=True)`);
|
|
173
|
+
}
|
|
174
|
+
for (const field of fields) {
|
|
175
|
+
lines.push(`${INDENT}${this.renderResultField(field)}`);
|
|
176
|
+
}
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
// --- Selection set walking ---------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Emit a `TypedDict` for `selectionSet` (a selection against `parentType`),
|
|
182
|
+
* named `typeName`. Nested object selections are emitted FIRST (deepest
|
|
183
|
+
* first) so references resolve top-to-bottom. Returns nothing; classes are
|
|
184
|
+
* pushed onto `out`.
|
|
185
|
+
*/
|
|
186
|
+
processSelectionSet(parentType, selectionSet, typeName, out) {
|
|
187
|
+
var _a, _b, _c, _d;
|
|
188
|
+
const fields = [];
|
|
189
|
+
const addField = (responseKey, annotation) => {
|
|
190
|
+
const key = resultFieldKey(responseKey, this._namingConvention);
|
|
191
|
+
// Record the wire (response) key so a `Field(alias=...)` is emitted when
|
|
192
|
+
// the Python attribute differs (snake_case conversion or keyword escape).
|
|
193
|
+
fields.push({ key, annotation, wire: responseKey });
|
|
194
|
+
};
|
|
195
|
+
for (const selection of selectionSet.selections) {
|
|
196
|
+
// Fragments (spread / inline) are not supported yet. Fail loudly instead
|
|
197
|
+
// of silently omitting their fields, which would otherwise produce a
|
|
198
|
+
// result type that looks valid but is missing data.
|
|
199
|
+
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
|
200
|
+
throw new Error(`@graphql-codegen/python-operations: fragment spread "...${selection.name.value}" on ` +
|
|
201
|
+
`"${parentType.name}" is not supported yet. Inline the fragment's fields directly into ` +
|
|
202
|
+
'the operation for now.');
|
|
203
|
+
}
|
|
204
|
+
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
205
|
+
const onType = (_b = (_a = selection.typeCondition) === null || _a === void 0 ? void 0 : _a.name.value) !== null && _b !== void 0 ? _b : parentType.name;
|
|
206
|
+
throw new Error(`@graphql-codegen/python-operations: inline fragment "... on ${onType}" on ` +
|
|
207
|
+
`"${parentType.name}" is not supported yet. Union/interface member selections cannot ` +
|
|
208
|
+
'be generated; select only common fields for now.');
|
|
209
|
+
}
|
|
210
|
+
const fieldNode = selection;
|
|
211
|
+
const fieldName = fieldNode.name.value;
|
|
212
|
+
const responseKey = (_d = (_c = fieldNode.alias) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : fieldName;
|
|
213
|
+
if (fieldName === '__typename') {
|
|
214
|
+
// `__typename` selections become a `Literal["ParentType"]` attribute.
|
|
215
|
+
// pydantic forbids a leading-underscore field name, so the attribute is
|
|
216
|
+
// `typename__` with `Field(alias='__typename')` pinning the wire key.
|
|
217
|
+
this._usesLiteral = true;
|
|
218
|
+
fields.push({
|
|
219
|
+
key: 'typename__',
|
|
220
|
+
annotation: `Literal["${parentType.name}"]`,
|
|
221
|
+
wire: '__typename',
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const fieldDef = parentType.getFields()[fieldName];
|
|
226
|
+
if (!fieldDef) {
|
|
227
|
+
// Unknown field (e.g. introspection on a type without it) — skip.
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const fieldType = fieldDef.type;
|
|
231
|
+
const namedType = getNamedType(fieldType);
|
|
232
|
+
if ((isObjectType(namedType) || isInterfaceType(namedType)) && fieldNode.selectionSet) {
|
|
233
|
+
const nestedName = `${typeName}${this.pascal(responseKey)}`;
|
|
234
|
+
// Recurse FIRST so deepest types are emitted before this one.
|
|
235
|
+
this.processSelectionSet(namedType, fieldNode.selectionSet, nestedName, out);
|
|
236
|
+
addField(responseKey, this.pyTypeFromGraphQL(fieldType, nestedName));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// Scalar / enum leaf (or union without object handling).
|
|
240
|
+
addField(responseKey, this.pyTypeFromGraphQL(fieldType, namedType.name));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
out.push(this.buildResultClass(typeName, fields));
|
|
244
|
+
}
|
|
245
|
+
buildVariables(variableDefinitions, variablesTypeName) {
|
|
246
|
+
const fields = variableDefinitions.map(def => {
|
|
247
|
+
// Variable keys are ALWAYS the wire name (sent to the server as-is).
|
|
248
|
+
const name = def.variable.name.value;
|
|
249
|
+
const annotation = this.pyTypeFromTypeNode(def.type);
|
|
250
|
+
return { key: name, annotation };
|
|
251
|
+
});
|
|
252
|
+
return this.buildVariablesClass(variablesTypeName, fields);
|
|
253
|
+
}
|
|
254
|
+
/** Generate all types for a single operation. Returns joined class blocks. */
|
|
255
|
+
generateOperation(operation) {
|
|
256
|
+
var _a, _b, _c;
|
|
257
|
+
const operationType = operation.operation; // 'query' | 'mutation' | 'subscription'
|
|
258
|
+
const operationTypeName = operationType.charAt(0).toUpperCase() + operationType.slice(1);
|
|
259
|
+
const operationName = (_b = (_a = operation.name) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : `Anonymous${this._anonymousCount++}`;
|
|
260
|
+
const baseName = `${this.pascal(operationName)}${operationTypeName}`;
|
|
261
|
+
const rootType = this.operationRootTypes[operationType];
|
|
262
|
+
const out = [];
|
|
263
|
+
if (rootType) {
|
|
264
|
+
this.processSelectionSet(rootType, operation.selectionSet, baseName, out);
|
|
265
|
+
}
|
|
266
|
+
out.push(this.buildVariables((_c = operation.variableDefinitions) !== null && _c !== void 0 ? _c : [], `${baseName}Variables`));
|
|
267
|
+
return out.join('\n\n\n');
|
|
268
|
+
}
|
|
269
|
+
/** Build the `prepend` import lines, emitting only what is referenced. */
|
|
270
|
+
getImports() {
|
|
271
|
+
const imports = [];
|
|
272
|
+
const typingParts = [];
|
|
273
|
+
if (this._usesOptional) {
|
|
274
|
+
typingParts.push('Optional');
|
|
275
|
+
}
|
|
276
|
+
// Every operation emits at least a variables TypedDict.
|
|
277
|
+
typingParts.push('TypedDict');
|
|
278
|
+
if (this._usesLiteral) {
|
|
279
|
+
typingParts.push('Literal');
|
|
280
|
+
}
|
|
281
|
+
if (this._usesAny) {
|
|
282
|
+
typingParts.push('Any');
|
|
283
|
+
}
|
|
284
|
+
imports.push(`from typing import ${typingParts.join(', ')}`);
|
|
285
|
+
if (this._usesBaseModel) {
|
|
286
|
+
// `ConfigDict` / `Field` are pydantic names; emit them from the same module
|
|
287
|
+
// as the base model, and only when the generated content uses them.
|
|
288
|
+
const parts = [this._baseModel];
|
|
289
|
+
if (this._usesConfigDict) {
|
|
290
|
+
parts.push('ConfigDict');
|
|
291
|
+
}
|
|
292
|
+
if (this._usesField) {
|
|
293
|
+
parts.push('Field');
|
|
294
|
+
}
|
|
295
|
+
imports.push(`from ${this._runtimeImport} import ${parts.join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
if (this._typesImport) {
|
|
298
|
+
imports.push(`from ${this._typesImport} import *`);
|
|
299
|
+
}
|
|
300
|
+
return imports;
|
|
301
|
+
}
|
|
302
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphql-codegen/python-operations",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.2-alpha-20261029093428-bb94591c48dc485d04089639f4ca0f124e339666",
|
|
4
|
+
"description": "Document-driven GraphQL Code Generator plugin that emits Python operation types (per-operation result + variables).",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@graphql-codegen/plugin-helpers": "^6.3.0",
|
|
11
|
+
"@graphql-codegen/python-common": "0.0.2-alpha-20261029093428-bb94591c48dc485d04089639f4ca0f124e339666",
|
|
12
|
+
"@graphql-codegen/visitor-plugin-common": "^6.3.0",
|
|
13
|
+
"tslib": "^2.8.1"
|
|
14
|
+
},
|
|
15
|
+
"repository": "git@github.com:dotansimha/graphql-code-generator-community.git",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">= 16.0.0"
|
|
19
|
+
},
|
|
20
|
+
"main": "cjs/index.js",
|
|
21
|
+
"module": "esm/index.js",
|
|
22
|
+
"typings": "typings/index.d.ts",
|
|
23
|
+
"typescript": {
|
|
24
|
+
"definition": "typings/index.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./typings/index.d.cts",
|
|
31
|
+
"default": "./cjs/index.js"
|
|
32
|
+
},
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./typings/index.d.ts",
|
|
35
|
+
"default": "./esm/index.js"
|
|
36
|
+
},
|
|
37
|
+
"default": {
|
|
38
|
+
"types": "./typings/index.d.ts",
|
|
39
|
+
"default": "./esm/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
}
|
|
6
44
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { RawDocumentsConfig } from '@graphql-codegen/visitor-plugin-common';
|
|
2
|
+
/**
|
|
3
|
+
* @description This plugin generates Python operation types (per-operation
|
|
4
|
+
* result + variables `TypedDict`s) from your GraphQL documents.
|
|
5
|
+
*
|
|
6
|
+
* It is document-driven (like `@graphql-codegen/typescript-operations`): it
|
|
7
|
+
* walks each `OperationDefinition` against the schema and recursively emits
|
|
8
|
+
* named, nested classes for every object selection set. Operation RESULT types
|
|
9
|
+
* become [pydantic](https://docs.pydantic.dev) `BaseModel` subclasses with
|
|
10
|
+
* snake_case attributes (each renamed key pinned via `Field(alias=...)`), while
|
|
11
|
+
* the `<Operation>Variables` type is a `TypedDict` keyed by the WIRE (GraphQL)
|
|
12
|
+
* variable name (variables are sent to the server as-is).
|
|
13
|
+
* Every annotation is quoted so forward / recursive references resolve without
|
|
14
|
+
* ordering constraints.
|
|
15
|
+
*/
|
|
16
|
+
export interface PythonOperationsPluginConfig extends RawDocumentsConfig {
|
|
17
|
+
/**
|
|
18
|
+
* @description Controls how GraphQL response keys (field names / aliases) are
|
|
19
|
+
* turned into Python `BaseModel` result-class attributes. Variable keys are
|
|
20
|
+
* unaffected — they always keep the wire (GraphQL) name.
|
|
21
|
+
*
|
|
22
|
+
* - `'snakeCase'` (default): keys are converted from camelCase to snake_case
|
|
23
|
+
* and Python reserved keywords are suffixed with `_`. Each renamed key is
|
|
24
|
+
* pinned with `Field(alias=...)` so pydantic hydrates the original wire key
|
|
25
|
+
* onto the attribute.
|
|
26
|
+
* - `'keep'`: response keys are used verbatim; a keyword / non-identifier key
|
|
27
|
+
* falls back to a trailing `_` for just that field.
|
|
28
|
+
*
|
|
29
|
+
* @default "snakeCase"
|
|
30
|
+
*/
|
|
31
|
+
namingConvention?: 'keep' | 'snakeCase';
|
|
32
|
+
/**
|
|
33
|
+
* @description The base class that generated operation result types extend.
|
|
34
|
+
* @default "BaseModel"
|
|
35
|
+
*/
|
|
36
|
+
baseModel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* @description The module the base model is imported from. Emitted as
|
|
39
|
+
* `from {runtimeImport} import {baseModel}` (with `ConfigDict` / `Field`
|
|
40
|
+
* appended when the generated types use aliases). Defaults to `pydantic`.
|
|
41
|
+
* @default "pydantic"
|
|
42
|
+
*/
|
|
43
|
+
runtimeImport?: string;
|
|
44
|
+
/**
|
|
45
|
+
* @description Optional module to star-import base schema type names from.
|
|
46
|
+
*
|
|
47
|
+
* Generated field annotations reference base type names (enums, input types)
|
|
48
|
+
* as quoted forward references, e.g. `role: 'Role'`. By default no import for
|
|
49
|
+
* those names is emitted (the plugin assumes the base types live in the same
|
|
50
|
+
* file or are provided elsewhere). If set, the plugin prepends
|
|
51
|
+
* `from {typesImport} import *` so those names resolve at runtime.
|
|
52
|
+
*
|
|
53
|
+
* @exampleMarkdown
|
|
54
|
+
* ```ts filename="codegen.ts"
|
|
55
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
56
|
+
*
|
|
57
|
+
* const config: CodegenConfig = {
|
|
58
|
+
* generates: {
|
|
59
|
+
* 'operations.py': {
|
|
60
|
+
* plugins: ['@graphql-codegen/python-operations'],
|
|
61
|
+
* config: {
|
|
62
|
+
* typesImport: 'my_package.schema_types',
|
|
63
|
+
* },
|
|
64
|
+
* },
|
|
65
|
+
* },
|
|
66
|
+
* };
|
|
67
|
+
* export default config;
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
typesImport?: string;
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { RawDocumentsConfig } from '@graphql-codegen/visitor-plugin-common';
|
|
2
|
+
/**
|
|
3
|
+
* @description This plugin generates Python operation types (per-operation
|
|
4
|
+
* result + variables `TypedDict`s) from your GraphQL documents.
|
|
5
|
+
*
|
|
6
|
+
* It is document-driven (like `@graphql-codegen/typescript-operations`): it
|
|
7
|
+
* walks each `OperationDefinition` against the schema and recursively emits
|
|
8
|
+
* named, nested classes for every object selection set. Operation RESULT types
|
|
9
|
+
* become [pydantic](https://docs.pydantic.dev) `BaseModel` subclasses with
|
|
10
|
+
* snake_case attributes (each renamed key pinned via `Field(alias=...)`), while
|
|
11
|
+
* the `<Operation>Variables` type is a `TypedDict` keyed by the WIRE (GraphQL)
|
|
12
|
+
* variable name (variables are sent to the server as-is).
|
|
13
|
+
* Every annotation is quoted so forward / recursive references resolve without
|
|
14
|
+
* ordering constraints.
|
|
15
|
+
*/
|
|
16
|
+
export interface PythonOperationsPluginConfig extends RawDocumentsConfig {
|
|
17
|
+
/**
|
|
18
|
+
* @description Controls how GraphQL response keys (field names / aliases) are
|
|
19
|
+
* turned into Python `BaseModel` result-class attributes. Variable keys are
|
|
20
|
+
* unaffected — they always keep the wire (GraphQL) name.
|
|
21
|
+
*
|
|
22
|
+
* - `'snakeCase'` (default): keys are converted from camelCase to snake_case
|
|
23
|
+
* and Python reserved keywords are suffixed with `_`. Each renamed key is
|
|
24
|
+
* pinned with `Field(alias=...)` so pydantic hydrates the original wire key
|
|
25
|
+
* onto the attribute.
|
|
26
|
+
* - `'keep'`: response keys are used verbatim; a keyword / non-identifier key
|
|
27
|
+
* falls back to a trailing `_` for just that field.
|
|
28
|
+
*
|
|
29
|
+
* @default "snakeCase"
|
|
30
|
+
*/
|
|
31
|
+
namingConvention?: 'keep' | 'snakeCase';
|
|
32
|
+
/**
|
|
33
|
+
* @description The base class that generated operation result types extend.
|
|
34
|
+
* @default "BaseModel"
|
|
35
|
+
*/
|
|
36
|
+
baseModel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* @description The module the base model is imported from. Emitted as
|
|
39
|
+
* `from {runtimeImport} import {baseModel}` (with `ConfigDict` / `Field`
|
|
40
|
+
* appended when the generated types use aliases). Defaults to `pydantic`.
|
|
41
|
+
* @default "pydantic"
|
|
42
|
+
*/
|
|
43
|
+
runtimeImport?: string;
|
|
44
|
+
/**
|
|
45
|
+
* @description Optional module to star-import base schema type names from.
|
|
46
|
+
*
|
|
47
|
+
* Generated field annotations reference base type names (enums, input types)
|
|
48
|
+
* as quoted forward references, e.g. `role: 'Role'`. By default no import for
|
|
49
|
+
* those names is emitted (the plugin assumes the base types live in the same
|
|
50
|
+
* file or are provided elsewhere). If set, the plugin prepends
|
|
51
|
+
* `from {typesImport} import *` so those names resolve at runtime.
|
|
52
|
+
*
|
|
53
|
+
* @exampleMarkdown
|
|
54
|
+
* ```ts filename="codegen.ts"
|
|
55
|
+
* import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
56
|
+
*
|
|
57
|
+
* const config: CodegenConfig = {
|
|
58
|
+
* generates: {
|
|
59
|
+
* 'operations.py': {
|
|
60
|
+
* plugins: ['@graphql-codegen/python-operations'],
|
|
61
|
+
* config: {
|
|
62
|
+
* typesImport: 'my_package.schema_types',
|
|
63
|
+
* },
|
|
64
|
+
* },
|
|
65
|
+
* },
|
|
66
|
+
* };
|
|
67
|
+
* export default config;
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
typesImport?: string;
|
|
71
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
|
|
2
|
+
import { PythonOperationsPluginConfig } from './config.cjs';
|
|
3
|
+
export * from './config.cjs';
|
|
4
|
+
export * from './visitor.cjs';
|
|
5
|
+
export * from './python-helpers.cjs';
|
|
6
|
+
/**
|
|
7
|
+
* Document-driven plugin: for each GraphQL operation in the provided documents,
|
|
8
|
+
* emit a per-operation result `TypedDict` (with named nested `TypedDict`s for
|
|
9
|
+
* every object selection) plus a `<Operation>Variables` `TypedDict`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const plugin: PluginFunction<PythonOperationsPluginConfig, Types.ComplexPluginOutput>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
|
|
2
|
+
import { PythonOperationsPluginConfig } from './config.js';
|
|
3
|
+
export * from './config.js';
|
|
4
|
+
export * from './visitor.js';
|
|
5
|
+
export * from './python-helpers.js';
|
|
6
|
+
/**
|
|
7
|
+
* Document-driven plugin: for each GraphQL operation in the provided documents,
|
|
8
|
+
* emit a per-operation result `TypedDict` (with named nested `TypedDict`s for
|
|
9
|
+
* every object selection) plus a `<Operation>Variables` `TypedDict`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const plugin: PluginFunction<PythonOperationsPluginConfig, Types.ComplexPluginOutput>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isReservedField, isValidClassAttr, pythonFieldName as pyFieldName, PYTHON_SCALARS } from '@graphql-codegen/python-common';
|
|
2
|
+
export { isReservedField, isValidClassAttr, PYTHON_SCALARS, pyFieldName };
|
|
3
|
+
export type PythonNamingConvention = 'keep' | 'snakeCase';
|
|
4
|
+
/**
|
|
5
|
+
* Compute the attribute name for a `BaseModel` result class. Under `'snakeCase'`
|
|
6
|
+
* the key is snake-cased (keywords suffixed with `_`); under `'keep'` the wire
|
|
7
|
+
* name is used verbatim, but — because `BaseModel` classes cannot use functional
|
|
8
|
+
* `TypedDict` syntax — a keyword / non-identifier name still falls back to a
|
|
9
|
+
* trailing `_`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resultFieldKey(name: string, convention: PythonNamingConvention): string;
|
|
12
|
+
/**
|
|
13
|
+
* Compute the `TypedDict` key for a GraphQL response / variable name under the
|
|
14
|
+
* given naming convention. Under `'keep'` the raw name is returned verbatim
|
|
15
|
+
* (functional `TypedDict` syntax handles invalid keys); under `'snakeCase'` the
|
|
16
|
+
* legacy snake_case + `_`-suffix behavior is applied.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fieldKey(name: string, convention: PythonNamingConvention): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isReservedField, isValidClassAttr, pythonFieldName as pyFieldName, PYTHON_SCALARS } from '@graphql-codegen/python-common';
|
|
2
|
+
export { isReservedField, isValidClassAttr, PYTHON_SCALARS, pyFieldName };
|
|
3
|
+
export type PythonNamingConvention = 'keep' | 'snakeCase';
|
|
4
|
+
/**
|
|
5
|
+
* Compute the attribute name for a `BaseModel` result class. Under `'snakeCase'`
|
|
6
|
+
* the key is snake-cased (keywords suffixed with `_`); under `'keep'` the wire
|
|
7
|
+
* name is used verbatim, but — because `BaseModel` classes cannot use functional
|
|
8
|
+
* `TypedDict` syntax — a keyword / non-identifier name still falls back to a
|
|
9
|
+
* trailing `_`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resultFieldKey(name: string, convention: PythonNamingConvention): string;
|
|
12
|
+
/**
|
|
13
|
+
* Compute the `TypedDict` key for a GraphQL response / variable name under the
|
|
14
|
+
* given naming convention. Under `'keep'` the raw name is returned verbatim
|
|
15
|
+
* (functional `TypedDict` syntax handles invalid keys); under `'snakeCase'` the
|
|
16
|
+
* legacy snake_case + `_`-suffix behavior is applied.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fieldKey(name: string, convention: PythonNamingConvention): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { GraphQLSchema, OperationDefinitionNode } from 'graphql';
|
|
2
|
+
import { BaseVisitor } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
+
import { PythonOperationsPluginConfig } from './config.cjs';
|
|
4
|
+
/**
|
|
5
|
+
* Walks GraphQL `OperationDefinition` nodes against the schema and recursively
|
|
6
|
+
* emits named, nested Python `TypedDict` classes for every object selection set,
|
|
7
|
+
* plus a `<Operation>Variables` `TypedDict` for the operation variables.
|
|
8
|
+
*
|
|
9
|
+
* Unlike the C-family `SelectionSetToObject`/`DeclarationBlock` approach (which
|
|
10
|
+
* emits inline anonymous objects), this mirrors the C#/Java strategy: every
|
|
11
|
+
* nested selection becomes its own named type referenced by its parent.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PythonOperationsVisitor extends BaseVisitor<PythonOperationsPluginConfig, any> {
|
|
14
|
+
private _schema;
|
|
15
|
+
private _typesImport?;
|
|
16
|
+
private _namingConvention;
|
|
17
|
+
private _baseModel;
|
|
18
|
+
private _runtimeImport;
|
|
19
|
+
private _usesOptional;
|
|
20
|
+
private _usesAny;
|
|
21
|
+
private _usesLiteral;
|
|
22
|
+
private _usesBaseModel;
|
|
23
|
+
private _usesConfigDict;
|
|
24
|
+
private _usesField;
|
|
25
|
+
private _anonymousCount;
|
|
26
|
+
constructor(schema: GraphQLSchema, rawConfig: PythonOperationsPluginConfig);
|
|
27
|
+
/** PascalCase a name segment without applying any prefix/suffix. */
|
|
28
|
+
private pascal;
|
|
29
|
+
private get operationRootTypes();
|
|
30
|
+
/**
|
|
31
|
+
* Build a Python annotation from a (resolved) GraphQL output type. The
|
|
32
|
+
* `objectBaseName` is the generated nested type name used when the base type
|
|
33
|
+
* is an object/interface/union selection.
|
|
34
|
+
*/
|
|
35
|
+
private pyTypeFromGraphQL;
|
|
36
|
+
private innerFromGraphQL;
|
|
37
|
+
/**
|
|
38
|
+
* Build a Python annotation from a variable `TypeNode`. Named types resolve
|
|
39
|
+
* to mapped scalars, or the bare base type name (enums / input objects).
|
|
40
|
+
*/
|
|
41
|
+
private pyTypeFromTypeNode;
|
|
42
|
+
private innerFromTypeNode;
|
|
43
|
+
/**
|
|
44
|
+
* Build a variables `TypedDict`. Variable keys are ALWAYS the wire (GraphQL)
|
|
45
|
+
* name — they are sent to the server as-is. If every key is a valid Python
|
|
46
|
+
* attribute the readable class syntax is used; otherwise (a key is a keyword /
|
|
47
|
+
* non-identifier) the functional `TypedDict('Name', { 'key': '...' })` syntax
|
|
48
|
+
* is emitted so the offending keys remain valid.
|
|
49
|
+
*/
|
|
50
|
+
private buildVariablesClass;
|
|
51
|
+
/**
|
|
52
|
+
* Emit the per-class `__init__` that pre-maps wire keys to their escaped
|
|
53
|
+
* attribute names before delegating to the base model. Only emitted for result
|
|
54
|
+
* classes that contain keyword fields.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* Render a single result field line: `key: 'annotation'`, with a pydantic
|
|
58
|
+
* `Field(alias=...)` when the Python attribute differs from the wire response
|
|
59
|
+
* key, and a `None` default for optional fields.
|
|
60
|
+
*/
|
|
61
|
+
private renderResultField;
|
|
62
|
+
/**
|
|
63
|
+
* Build a result base-model subclass. A field whose Python attribute differs
|
|
64
|
+
* from the wire key is pinned with `Field(alias=...)`, and a
|
|
65
|
+
* `model_config = ConfigDict(populate_by_name=True)` line is prepended so such
|
|
66
|
+
* classes can also be built by attribute name.
|
|
67
|
+
*/
|
|
68
|
+
private buildResultClass;
|
|
69
|
+
/**
|
|
70
|
+
* Emit a `TypedDict` for `selectionSet` (a selection against `parentType`),
|
|
71
|
+
* named `typeName`. Nested object selections are emitted FIRST (deepest
|
|
72
|
+
* first) so references resolve top-to-bottom. Returns nothing; classes are
|
|
73
|
+
* pushed onto `out`.
|
|
74
|
+
*/
|
|
75
|
+
private processSelectionSet;
|
|
76
|
+
private buildVariables;
|
|
77
|
+
/** Generate all types for a single operation. Returns joined class blocks. */
|
|
78
|
+
generateOperation(operation: OperationDefinitionNode): string;
|
|
79
|
+
/** Build the `prepend` import lines, emitting only what is referenced. */
|
|
80
|
+
getImports(): string[];
|
|
81
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { GraphQLSchema, OperationDefinitionNode } from 'graphql';
|
|
2
|
+
import { BaseVisitor } from '@graphql-codegen/visitor-plugin-common';
|
|
3
|
+
import { PythonOperationsPluginConfig } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Walks GraphQL `OperationDefinition` nodes against the schema and recursively
|
|
6
|
+
* emits named, nested Python `TypedDict` classes for every object selection set,
|
|
7
|
+
* plus a `<Operation>Variables` `TypedDict` for the operation variables.
|
|
8
|
+
*
|
|
9
|
+
* Unlike the C-family `SelectionSetToObject`/`DeclarationBlock` approach (which
|
|
10
|
+
* emits inline anonymous objects), this mirrors the C#/Java strategy: every
|
|
11
|
+
* nested selection becomes its own named type referenced by its parent.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PythonOperationsVisitor extends BaseVisitor<PythonOperationsPluginConfig, any> {
|
|
14
|
+
private _schema;
|
|
15
|
+
private _typesImport?;
|
|
16
|
+
private _namingConvention;
|
|
17
|
+
private _baseModel;
|
|
18
|
+
private _runtimeImport;
|
|
19
|
+
private _usesOptional;
|
|
20
|
+
private _usesAny;
|
|
21
|
+
private _usesLiteral;
|
|
22
|
+
private _usesBaseModel;
|
|
23
|
+
private _usesConfigDict;
|
|
24
|
+
private _usesField;
|
|
25
|
+
private _anonymousCount;
|
|
26
|
+
constructor(schema: GraphQLSchema, rawConfig: PythonOperationsPluginConfig);
|
|
27
|
+
/** PascalCase a name segment without applying any prefix/suffix. */
|
|
28
|
+
private pascal;
|
|
29
|
+
private get operationRootTypes();
|
|
30
|
+
/**
|
|
31
|
+
* Build a Python annotation from a (resolved) GraphQL output type. The
|
|
32
|
+
* `objectBaseName` is the generated nested type name used when the base type
|
|
33
|
+
* is an object/interface/union selection.
|
|
34
|
+
*/
|
|
35
|
+
private pyTypeFromGraphQL;
|
|
36
|
+
private innerFromGraphQL;
|
|
37
|
+
/**
|
|
38
|
+
* Build a Python annotation from a variable `TypeNode`. Named types resolve
|
|
39
|
+
* to mapped scalars, or the bare base type name (enums / input objects).
|
|
40
|
+
*/
|
|
41
|
+
private pyTypeFromTypeNode;
|
|
42
|
+
private innerFromTypeNode;
|
|
43
|
+
/**
|
|
44
|
+
* Build a variables `TypedDict`. Variable keys are ALWAYS the wire (GraphQL)
|
|
45
|
+
* name — they are sent to the server as-is. If every key is a valid Python
|
|
46
|
+
* attribute the readable class syntax is used; otherwise (a key is a keyword /
|
|
47
|
+
* non-identifier) the functional `TypedDict('Name', { 'key': '...' })` syntax
|
|
48
|
+
* is emitted so the offending keys remain valid.
|
|
49
|
+
*/
|
|
50
|
+
private buildVariablesClass;
|
|
51
|
+
/**
|
|
52
|
+
* Emit the per-class `__init__` that pre-maps wire keys to their escaped
|
|
53
|
+
* attribute names before delegating to the base model. Only emitted for result
|
|
54
|
+
* classes that contain keyword fields.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* Render a single result field line: `key: 'annotation'`, with a pydantic
|
|
58
|
+
* `Field(alias=...)` when the Python attribute differs from the wire response
|
|
59
|
+
* key, and a `None` default for optional fields.
|
|
60
|
+
*/
|
|
61
|
+
private renderResultField;
|
|
62
|
+
/**
|
|
63
|
+
* Build a result base-model subclass. A field whose Python attribute differs
|
|
64
|
+
* from the wire key is pinned with `Field(alias=...)`, and a
|
|
65
|
+
* `model_config = ConfigDict(populate_by_name=True)` line is prepended so such
|
|
66
|
+
* classes can also be built by attribute name.
|
|
67
|
+
*/
|
|
68
|
+
private buildResultClass;
|
|
69
|
+
/**
|
|
70
|
+
* Emit a `TypedDict` for `selectionSet` (a selection against `parentType`),
|
|
71
|
+
* named `typeName`. Nested object selections are emitted FIRST (deepest
|
|
72
|
+
* first) so references resolve top-to-bottom. Returns nothing; classes are
|
|
73
|
+
* pushed onto `out`.
|
|
74
|
+
*/
|
|
75
|
+
private processSelectionSet;
|
|
76
|
+
private buildVariables;
|
|
77
|
+
/** Generate all types for a single operation. Returns joined class blocks. */
|
|
78
|
+
generateOperation(operation: OperationDefinitionNode): string;
|
|
79
|
+
/** Build the `prepend` import lines, emitting only what is referenced. */
|
|
80
|
+
getImports(): string[];
|
|
81
|
+
}
|