instant-cli 1.0.40 → 1.0.41-branch-python-sdk-v1.26586025551.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
 
2
- > instant-cli@1.0.40 build /home/runner/work/instant/instant/client/packages/cli
2
+ > instant-cli@1.0.41-branch-python-sdk-v1.26586025551.1 build /home/runner/work/instant/instant/client/packages/cli
3
3
  > rm -rf dist; tsc -p tsconfig.build.json
4
4
 
@@ -0,0 +1,39 @@
1
+ import { FileSystem, Path } from '@effect/platform';
2
+ import { Effect, Schema } from 'effect';
3
+ import { ReadSchemaFileError, SchemaValidationError } from '../lib/pushSchema.ts';
4
+ declare const GenpyWriteError_base: Schema.TaggedErrorClass<GenpyWriteError, "GenpyWriteError", {
5
+ readonly _tag: Schema.tag<"GenpyWriteError">;
6
+ } & {
7
+ message: typeof Schema.String;
8
+ }>;
9
+ export declare class GenpyWriteError extends GenpyWriteError_base {
10
+ }
11
+ export declare const genpyCommand: ({ outDir }: {
12
+ outDir?: string;
13
+ }) => Effect.Effect<undefined, ReadSchemaFileError | SchemaValidationError | GenpyWriteError, FileSystem.FileSystem | Path.Path>;
14
+ type SchemaLike = {
15
+ entities: Record<string, EntityDefLike>;
16
+ links: Record<string, LinkDefLike>;
17
+ };
18
+ type EntityDefLike = {
19
+ attrs: Record<string, DataAttrDefLike>;
20
+ };
21
+ type DataAttrDefLike = {
22
+ valueType: 'string' | 'number' | 'boolean' | 'date' | 'json';
23
+ required: boolean;
24
+ };
25
+ type LinkDefLike = {
26
+ forward: {
27
+ on: string;
28
+ label: string;
29
+ has: 'one' | 'many';
30
+ };
31
+ reverse: {
32
+ on: string;
33
+ label: string;
34
+ has: 'one' | 'many';
35
+ };
36
+ };
37
+ export declare function buildPy(schema: SchemaLike): string;
38
+ export {};
39
+ //# sourceMappingURL=genpy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genpy.d.ts","sourceRoot":"","sources":["../../src/commands/genpy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;;;;;;AAE9B,qBAAa,eAAgB,SAAQ,oBAInC;CAAG;AAML,eAAO,MAAM,YAAY,GAAI,YAAY;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,+HAuDxD,CAAC;AAIL,KAAK,UAAU,GAAG;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACxC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC7D,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;IAC5D,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;CAC7D,CAAC;AAyIF,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAoNlD"}
@@ -0,0 +1,537 @@
1
+ import { FileSystem, Path } from '@effect/platform';
2
+ import chalk from 'chalk';
3
+ import { Effect, Schema } from 'effect';
4
+ import { readLocalSchemaFile } from '../old.js';
5
+ import { ReadSchemaFileError, SchemaValidationError, } from "../lib/pushSchema.js";
6
+ export class GenpyWriteError extends Schema.TaggedError('GenpyWriteError')('GenpyWriteError', {
7
+ message: Schema.String,
8
+ }) {
9
+ }
10
+ const PY_HEADER = `# AUTOGENERATED by \`instant-cli genpy\`. DO NOT EDIT THIS FILE BY HAND.
11
+ # Re-run \`npx instant-cli genpy\` after schema changes.
12
+ `;
13
+ export const genpyCommand = ({ outDir }) => Effect.gen(function* () {
14
+ const localSchemaFile = yield* Effect.tryPromise({
15
+ try: readLocalSchemaFile,
16
+ catch: (e) => e instanceof Error
17
+ ? ReadSchemaFileError.make({ message: e.message })
18
+ : ReadSchemaFileError.make({ message: String(e) }),
19
+ });
20
+ if (!localSchemaFile || !localSchemaFile.schema) {
21
+ return yield* ReadSchemaFileError.make({
22
+ message: `We couldn't find your ${chalk.yellow('`instant.schema.ts`')} file. Make sure it's in the project root or under \`src/\`. (Hint: set INSTANT_SCHEMA_FILE_PATH to override.)`,
23
+ });
24
+ }
25
+ if (localSchemaFile.schema.constructor?.name !== 'InstantSchemaDef') {
26
+ return yield* SchemaValidationError.make({
27
+ message: `We couldn't find your schema export.\nIn your ${chalk.yellow('`instant.schema.ts`')} file, make sure you ${chalk.green('`export default schema`')}`,
28
+ });
29
+ }
30
+ const collision = findIdentifierCollisions(localSchemaFile.schema);
31
+ if (collision) {
32
+ return yield* SchemaValidationError.make({ message: collision });
33
+ }
34
+ const path = yield* Path.Path;
35
+ const fs = yield* FileSystem.FileSystem;
36
+ // Default: write alongside the schema; `--out-dir` overrides for
37
+ // monorepos where Python lives in a sibling directory.
38
+ const targetDir = outDir
39
+ ? path.resolve(outDir)
40
+ : path.dirname(localSchemaFile.path);
41
+ yield* fs.makeDirectory(targetDir, { recursive: true }).pipe(Effect.mapError((e) => GenpyWriteError.make({
42
+ message: `Failed to create ${targetDir}: ${e}`,
43
+ })));
44
+ const pyPath = path.join(targetDir, 'instant_types.py');
45
+ const staleStub = path.join(targetDir, 'instant_types.pyi');
46
+ const pyContent = buildPy(localSchemaFile.schema);
47
+ yield* fs
48
+ .writeFileString(pyPath, pyContent)
49
+ .pipe(Effect.mapError((e) => GenpyWriteError.make({ message: `Failed to write ${pyPath}: ${e}` })));
50
+ // Earlier versions of genpy emitted a separate `.pyi`; drop it if a
51
+ // user is upgrading so it doesn't shadow the new `.py`.
52
+ yield* fs.remove(staleStub).pipe(Effect.ignore);
53
+ yield* Effect.log(`✅ Wrote ${pyPath}`);
54
+ });
55
+ // In python-land we would do something like keyword.iskeyword(), to generate
56
+ // this list but because our gen-script is in ts-land we use an explicit set
57
+ // based on Python 3 keywords
58
+ // https://docs.python.org/3/reference/lexical_analysis.html#keywords
59
+ const PY_KEYWORDS = new Set([
60
+ 'False',
61
+ 'None',
62
+ 'True',
63
+ 'and',
64
+ 'as',
65
+ 'assert',
66
+ 'async',
67
+ 'await',
68
+ 'break',
69
+ 'class',
70
+ 'continue',
71
+ 'def',
72
+ 'del',
73
+ 'elif',
74
+ 'else',
75
+ 'except',
76
+ 'finally',
77
+ 'for',
78
+ 'from',
79
+ 'global',
80
+ 'if',
81
+ 'import',
82
+ 'in',
83
+ 'is',
84
+ 'lambda',
85
+ 'nonlocal',
86
+ 'not',
87
+ 'or',
88
+ 'pass',
89
+ 'raise',
90
+ 'return',
91
+ 'try',
92
+ 'while',
93
+ 'with',
94
+ 'yield',
95
+ ]);
96
+ function isPyIdent(name) {
97
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !PY_KEYWORDS.has(name);
98
+ }
99
+ function safePyIdent(name) {
100
+ if (isPyIdent(name))
101
+ return name;
102
+ let safe = name.replace(/[^A-Za-z0-9_]+/g, '_').replace(/_+/g, '_');
103
+ if (safe === '' || /^[0-9]/.test(safe))
104
+ safe = '_' + safe;
105
+ if (PY_KEYWORDS.has(safe))
106
+ safe += '_';
107
+ return safe;
108
+ }
109
+ // Python silently keeps the last duplicate class attribute, dropping
110
+ // the first field's alias mapping. Reject up front instead.
111
+ function findIdentifierCollisions(schema) {
112
+ for (const entName of Object.keys(schema.entities)) {
113
+ const seen = new Map([['id', 'implicit `id`']]);
114
+ const check = (rawKey, kind) => {
115
+ const safe = safePyIdent(rawKey);
116
+ const existing = seen.get(safe);
117
+ if (existing !== undefined) {
118
+ return (`Entity "${entName}": ${existing} and ${kind} "${rawKey}" both ` +
119
+ `map to the Python identifier "${safe}". Rename one in your schema.`);
120
+ }
121
+ seen.set(safe, `${kind} "${rawKey}"`);
122
+ return undefined;
123
+ };
124
+ for (const attrName of Object.keys(schema.entities[entName].attrs)) {
125
+ const err = check(attrName, 'attribute');
126
+ if (err)
127
+ return err;
128
+ }
129
+ for (const link of Object.values(schema.links)) {
130
+ if (link.forward.on === entName) {
131
+ const err = check(link.forward.label, 'link');
132
+ if (err)
133
+ return err;
134
+ }
135
+ if (link.reverse.on === entName) {
136
+ const err = check(link.reverse.label, 'link');
137
+ if (err)
138
+ return err;
139
+ }
140
+ }
141
+ }
142
+ return undefined;
143
+ }
144
+ function className(entName) {
145
+ // Strip a leading `$` and PascalCase across separators so `my-entity`
146
+ // or `$users` produce valid Python class names.
147
+ const stripped = entName.startsWith('$') ? entName.slice(1) : entName;
148
+ return stripped
149
+ .split(/[-_\s]+/)
150
+ .filter(Boolean)
151
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
152
+ .join('');
153
+ }
154
+ function mapValueType(vt) {
155
+ switch (vt) {
156
+ case 'string':
157
+ return 'str';
158
+ case 'number':
159
+ return 'float';
160
+ case 'boolean':
161
+ return 'bool';
162
+ case 'date':
163
+ return 'datetime';
164
+ case 'json':
165
+ return 'Any';
166
+ default:
167
+ // Defensive: if a new valueType ever lands in the schema without a
168
+ // genpy update, fall back to Any so generated code stays valid Python.
169
+ return 'Any';
170
+ }
171
+ }
172
+ function mapWriteValueType(vt) {
173
+ if (vt === 'date') {
174
+ return 'datetime | str | int | float';
175
+ }
176
+ return mapValueType(vt);
177
+ }
178
+ export function buildPy(schema) {
179
+ const entityNames = Object.keys(schema.entities).sort();
180
+ const fieldsByEntity = {};
181
+ const linkFieldsByEntity = {};
182
+ let usesDatetime = false;
183
+ for (const entName of entityNames) {
184
+ fieldsByEntity[entName] = [{ name: 'id', type: 'str', hasDefault: false }];
185
+ linkFieldsByEntity[entName] = [];
186
+ for (const [attrName, attrDef] of Object.entries(schema.entities[entName].attrs)) {
187
+ const baseType = mapValueType(attrDef.valueType);
188
+ if (baseType === 'datetime')
189
+ usesDatetime = true;
190
+ const type = attrDef.required ? baseType : `${baseType} | None`;
191
+ const safeName = safePyIdent(attrName);
192
+ fieldsByEntity[entName].push({
193
+ name: safeName,
194
+ type,
195
+ hasDefault: !attrDef.required,
196
+ rawKey: safeName === attrName ? undefined : attrName,
197
+ });
198
+ }
199
+ }
200
+ for (const link of Object.values(schema.links)) {
201
+ const fwd = link.forward;
202
+ const rev = link.reverse;
203
+ if (linkFieldsByEntity[fwd.on]) {
204
+ const safeName = safePyIdent(fwd.label);
205
+ linkFieldsByEntity[fwd.on].push({
206
+ name: safeName,
207
+ type: fwd.has === 'one'
208
+ ? `${className(rev.on)} | None`
209
+ : `list[${className(rev.on)}] | None`,
210
+ hasDefault: true,
211
+ rawKey: safeName === fwd.label ? undefined : fwd.label,
212
+ });
213
+ }
214
+ if (linkFieldsByEntity[rev.on]) {
215
+ const safeName = safePyIdent(rev.label);
216
+ linkFieldsByEntity[rev.on].push({
217
+ name: safeName,
218
+ type: rev.has === 'one'
219
+ ? `${className(fwd.on)} | None`
220
+ : `list[${className(fwd.on)}] | None`,
221
+ hasDefault: true,
222
+ rawKey: safeName === rev.label ? undefined : rev.label,
223
+ });
224
+ }
225
+ }
226
+ // Sort link fields alphabetically for stable output across schema rewrites.
227
+ for (const entName of entityNames) {
228
+ linkFieldsByEntity[entName].sort((a, b) => a.name.localeCompare(b.name));
229
+ }
230
+ let usesField = false;
231
+ const classBlocks = entityNames.map((entName) => {
232
+ const allFields = [
233
+ ...fieldsByEntity[entName],
234
+ ...linkFieldsByEntity[entName],
235
+ ];
236
+ const hasAlias = allFields.some((f) => f.rawKey !== undefined);
237
+ const config = hasAlias
238
+ ? 'ConfigDict(extra="ignore", populate_by_name=True)'
239
+ : 'ConfigDict(extra="ignore")';
240
+ const lines = [
241
+ `class ${className(entName)}(BaseModel):`,
242
+ ` model_config = ${config}`,
243
+ ];
244
+ for (const f of allFields) {
245
+ let decl;
246
+ if (f.rawKey !== undefined) {
247
+ usesField = true;
248
+ const args = f.hasDefault
249
+ ? `default=None, alias=${JSON.stringify(f.rawKey)}`
250
+ : `alias=${JSON.stringify(f.rawKey)}`;
251
+ decl = ` ${f.name}: ${f.type} = Field(${args})`;
252
+ }
253
+ else if (f.hasDefault) {
254
+ decl = ` ${f.name}: ${f.type} = None`;
255
+ }
256
+ else {
257
+ decl = ` ${f.name}: ${f.type}`;
258
+ }
259
+ lines.push(decl);
260
+ }
261
+ return lines.join('\n');
262
+ });
263
+ const rebuilds = entityNames.map((entName) => `${className(entName)}.model_rebuild()`);
264
+ // Webhook record / handler types live in the same .py so users get one
265
+ // import point (`from instant_types import ...`) for both query results
266
+ // and webhook handler type-checking.
267
+ const webhookSection = buildWebhookSection(schema, entityNames);
268
+ const typingImports = [
269
+ 'TYPE_CHECKING',
270
+ 'Any',
271
+ 'Callable',
272
+ 'Literal',
273
+ 'TypedDict',
274
+ ];
275
+ const stdlibImports = [];
276
+ if (usesDatetime)
277
+ stdlibImports.push('from datetime import datetime');
278
+ stdlibImports.push(`from typing import ${typingImports.join(', ')}`);
279
+ const pydanticImports = usesField
280
+ ? 'BaseModel, ConfigDict, Field'
281
+ : 'BaseModel, ConfigDict';
282
+ const sdkImports = [
283
+ 'from instantdb import AsyncInstant as _BaseAsyncInstant',
284
+ 'from instantdb import Instant as _BaseInstant',
285
+ 'from instantdb import InstantAPIError, InstantError, id, lookup',
286
+ ].join('\n');
287
+ const importBlock = stdlibImports.join('\n') +
288
+ '\n\n' +
289
+ `from pydantic import ${pydanticImports}\n\n` +
290
+ sdkImports;
291
+ const txTypingBlock = buildTxTypingBlock(schema, entityNames);
292
+ // Add record-model rebuilds so forward-string annotations to entity
293
+ // types resolve under `from __future__ import annotations`.
294
+ const recordRebuilds = [];
295
+ for (const entName of entityNames) {
296
+ const cls = className(entName);
297
+ for (const action of ['Create', 'Update', 'Delete']) {
298
+ recordRebuilds.push(`${cls}${action}Record.model_rebuild()`);
299
+ }
300
+ }
301
+ const entityMapLines = [' "entities": {'];
302
+ for (const entName of entityNames) {
303
+ entityMapLines.push(` ${JSON.stringify(entName)}: ${className(entName)},`);
304
+ }
305
+ entityMapLines.push(' },');
306
+ const recordMapLines = [' "records": {'];
307
+ for (const entName of entityNames) {
308
+ const cls = className(entName);
309
+ for (const action of ['create', 'update', 'delete']) {
310
+ const capAction = action.charAt(0).toUpperCase() + action.slice(1);
311
+ recordMapLines.push(` (${JSON.stringify(entName)}, ${JSON.stringify(action)}): ${cls}${capAction}Record,`);
312
+ }
313
+ }
314
+ recordMapLines.push(' },');
315
+ const schemaLiteral = [
316
+ 'schema = {',
317
+ ...entityMapLines,
318
+ ...recordMapLines,
319
+ '}',
320
+ ].join('\n');
321
+ // `_schema` defaults via setdefault so `_clone()` (used by `as_user`)
322
+ // can pass an explicit value without a double-pass TypeError.
323
+ const typedClients = [
324
+ 'class Instant(_BaseInstant):',
325
+ ' tx: _TxBuilder',
326
+ '',
327
+ ' def __init__(self, **kwargs: Any) -> None:',
328
+ ' kwargs.setdefault("_schema", schema)',
329
+ ' super().__init__(**kwargs)',
330
+ '',
331
+ '',
332
+ 'class AsyncInstant(_BaseAsyncInstant):',
333
+ ' tx: _TxBuilder',
334
+ '',
335
+ ' def __init__(self, **kwargs: Any) -> None:',
336
+ ' kwargs.setdefault("_schema", schema)',
337
+ ' super().__init__(**kwargs)',
338
+ ].join('\n');
339
+ return [
340
+ PY_HEADER + '"""Schema-bound Instant clients + Pydantic models."""',
341
+ '',
342
+ 'from __future__ import annotations',
343
+ '',
344
+ importBlock,
345
+ '',
346
+ '',
347
+ txTypingBlock,
348
+ '',
349
+ '',
350
+ classBlocks.join('\n\n\n'),
351
+ '',
352
+ '',
353
+ '# Resolve forward references introduced by `from __future__ import annotations`.',
354
+ rebuilds.join('\n'),
355
+ '',
356
+ '',
357
+ webhookSection,
358
+ '',
359
+ '',
360
+ '# Rebuild webhook records so `before` / `after` annotations resolve to entity classes.',
361
+ recordRebuilds.join('\n'),
362
+ '',
363
+ '',
364
+ schemaLiteral,
365
+ '',
366
+ '',
367
+ typedClients,
368
+ '',
369
+ ].join('\n');
370
+ }
371
+ // ---------- webhook record / handler codegen ----------
372
+ function buildWebhookSection(schema, entityNames) {
373
+ // Schemas with zero entities would produce `WebhookRecord = ` (no RHS).
374
+ if (entityNames.length === 0)
375
+ return '';
376
+ const recordClasses = [];
377
+ const namespaceUnions = [];
378
+ const namespaceHandlerDicts = [];
379
+ // `before`/`after` shape varies by action.
380
+ const RECORD_VARIANTS = [
381
+ { action: 'create', before: 'None = None', afterTpl: (c) => c },
382
+ { action: 'update', before: 'CLS', afterTpl: (c) => c },
383
+ { action: 'delete', before: 'CLS', afterTpl: () => 'None = None' },
384
+ ];
385
+ for (const entName of entityNames) {
386
+ const cls = className(entName);
387
+ for (const { action, before, afterTpl } of RECORD_VARIANTS) {
388
+ const cap = action.charAt(0).toUpperCase() + action.slice(1);
389
+ recordClasses.push([
390
+ `class ${cls}${cap}Record(BaseModel):`,
391
+ ` model_config = ConfigDict(extra="ignore")`,
392
+ ` namespace: Literal[${JSON.stringify(entName)}] = ${JSON.stringify(entName)}`,
393
+ ` id: str`,
394
+ ` action: Literal[${JSON.stringify(action)}] = ${JSON.stringify(action)}`,
395
+ ` before: ${before === 'CLS' ? cls : before}`,
396
+ ` after: ${afterTpl(cls)}`,
397
+ ` idempotencyKey: str`,
398
+ ].join('\n'));
399
+ }
400
+ namespaceUnions.push(`${cls}Record = ${cls}CreateRecord | ${cls}UpdateRecord | ${cls}DeleteRecord`);
401
+ // Per-namespace handler TypedDict. Functional syntax so the `$default`
402
+ // key is expressible.
403
+ namespaceHandlerDicts.push([
404
+ `_${cls}Handlers = TypedDict(`,
405
+ ` "_${cls}Handlers",`,
406
+ ' {',
407
+ ` "create": Callable[[${cls}CreateRecord], Any],`,
408
+ ` "update": Callable[[${cls}UpdateRecord], Any],`,
409
+ ` "delete": Callable[[${cls}DeleteRecord], Any],`,
410
+ ` "$default": Callable[[${cls}Record], Any],`,
411
+ ' },',
412
+ ' total=False,',
413
+ ')',
414
+ ].join('\n'));
415
+ }
416
+ const rootUnionTerms = entityNames.map((n) => `${className(n)}Record`);
417
+ const rootUnion = `WebhookRecord = ${rootUnionTerms.join(' | ')}`;
418
+ const handlerDictLines = [
419
+ 'WebhookHandlers = TypedDict(',
420
+ ' "WebhookHandlers",',
421
+ ' {',
422
+ ];
423
+ for (const entName of entityNames) {
424
+ handlerDictLines.push(` ${JSON.stringify(entName)}: _${className(entName)}Handlers,`);
425
+ }
426
+ handlerDictLines.push(` "$default": Callable[[WebhookRecord], Any],`);
427
+ handlerDictLines.push(' },', ' total=False,', ')');
428
+ return [
429
+ '# ---------- webhook record models ----------',
430
+ '',
431
+ recordClasses.join('\n\n\n'),
432
+ '',
433
+ '',
434
+ '# ---------- webhook handler types ----------',
435
+ '',
436
+ namespaceUnions.join('\n'),
437
+ '',
438
+ rootUnion,
439
+ '',
440
+ '',
441
+ namespaceHandlerDicts.join('\n\n\n'),
442
+ '',
443
+ '',
444
+ handlerDictLines.join('\n'),
445
+ ].join('\n');
446
+ }
447
+ // ---------- tx builder typing (TYPE_CHECKING block) ----------
448
+ function buildTxTypingBlock(schema, entityNames) {
449
+ const linkFieldsByEntity = {};
450
+ for (const entName of entityNames)
451
+ linkFieldsByEntity[entName] = [];
452
+ for (const link of Object.values(schema.links)) {
453
+ const fwdArg = link.forward.has === 'one' ? 'str' : 'str | list[str]';
454
+ const revArg = link.reverse.has === 'one' ? 'str' : 'str | list[str]';
455
+ if (linkFieldsByEntity[link.forward.on]) {
456
+ linkFieldsByEntity[link.forward.on].push({
457
+ label: link.forward.label,
458
+ type: fwdArg,
459
+ });
460
+ }
461
+ if (linkFieldsByEntity[link.reverse.on]) {
462
+ linkFieldsByEntity[link.reverse.on].push({
463
+ label: link.reverse.label,
464
+ type: revArg,
465
+ });
466
+ }
467
+ }
468
+ for (const entName of entityNames) {
469
+ linkFieldsByEntity[entName].sort((a, b) => a.label.localeCompare(b.label));
470
+ }
471
+ // Class-syntax TypedDict requires identifier keys; switch to the
472
+ // functional form when any key on a given dict is irregular.
473
+ const buildTypedDict = (name, fields) => {
474
+ const allIdent = fields.every((f) => isPyIdent(f.key));
475
+ if (allIdent) {
476
+ const lines = [` class ${name}(TypedDict, total=False):`];
477
+ for (const f of fields)
478
+ lines.push(` ${f.key}: ${f.type}`);
479
+ if (fields.length === 0)
480
+ lines.push(' pass');
481
+ return lines;
482
+ }
483
+ const lines = [
484
+ ` ${name} = TypedDict(`,
485
+ ` ${JSON.stringify(name)},`,
486
+ ' {',
487
+ ];
488
+ for (const f of fields) {
489
+ lines.push(` ${JSON.stringify(f.key)}: ${f.type},`);
490
+ }
491
+ lines.push(' },', ' total=False,', ' )');
492
+ return lines;
493
+ };
494
+ const lines = [
495
+ 'if TYPE_CHECKING:',
496
+ ' from typing import Generic, TypeVar, overload',
497
+ '',
498
+ ' ChunkT = TypeVar("ChunkT")',
499
+ '',
500
+ ...buildTypedDict('_TxOpts', [{ key: 'upsert', type: 'bool' }]),
501
+ ];
502
+ for (const entName of entityNames) {
503
+ const cls = className(entName);
504
+ const argFields = [];
505
+ for (const [attrName, attrDef] of Object.entries(schema.entities[entName].attrs)) {
506
+ argFields.push({
507
+ key: attrName,
508
+ type: mapWriteValueType(attrDef.valueType),
509
+ });
510
+ }
511
+ lines.push('');
512
+ lines.push(...buildTypedDict(`${cls}Args`, argFields));
513
+ lines.push('');
514
+ lines.push(...buildTypedDict(`${cls}Links`, linkFieldsByEntity[entName].map((l) => ({
515
+ key: l.label,
516
+ type: l.type,
517
+ }))));
518
+ lines.push('');
519
+ lines.push(` class _${cls}Chunk:`, ` def update(self, args: ${cls}Args, opts: _TxOpts | None = None) -> _${cls}Chunk: ...`, ` def create(self, args: ${cls}Args, opts: _TxOpts | None = None) -> _${cls}Chunk: ...`, ` def link(self, args: ${cls}Links) -> _${cls}Chunk: ...`, ` def unlink(self, args: ${cls}Links) -> _${cls}Chunk: ...`, ` def delete(self) -> _${cls}Chunk: ...`, ` def merge(self, args: ${cls}Args, opts: _TxOpts | None = None) -> _${cls}Chunk: ...`, ` def rule_params(self, args: dict[str, Any]) -> _${cls}Chunk: ...`);
520
+ }
521
+ lines.push('', ' class _NamespaceBuilder(Generic[ChunkT]):', ' def __getitem__(self, eid: Any) -> ChunkT: ...', ' def lookup(self, attr: str, value: Any) -> ChunkT: ...', '');
522
+ // Only valid Python identifiers can appear as `_TxBuilder` attrs (for
523
+ // `db.tx.<name>` autocomplete); irregular names use `db.tx["..."]`.
524
+ const identifierEntities = entityNames.filter(isPyIdent);
525
+ lines.push(' class _TxBuilder:');
526
+ for (const entName of entityNames) {
527
+ lines.push(` @overload`, ` def __getitem__(self, etype: Literal[${JSON.stringify(entName)}]) -> _NamespaceBuilder[_${className(entName)}Chunk]: ...`);
528
+ }
529
+ lines.push(' @overload');
530
+ lines.push(' def __getitem__(self, etype: str) -> _NamespaceBuilder[Any]: ...');
531
+ lines.push(' def __getitem__(self, etype: str) -> _NamespaceBuilder[Any]: ...');
532
+ for (const entName of identifierEntities) {
533
+ lines.push(` ${entName}: _NamespaceBuilder[_${className(entName)}Chunk]`);
534
+ }
535
+ return lines.join('\n');
536
+ }
537
+ //# sourceMappingURL=genpy.js.map