Skip to content

Commit 586b5a6

Browse files
committed
feat: wip python generator
1 parent f4ed51c commit 586b5a6

5 files changed

Lines changed: 457 additions & 9 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import {stripIndent} from 'common-tags'
2+
import camelCase from 'lodash.camelcase'
3+
4+
import {ZodToPythonClassMapper} from '../language-mappers/zod-to-python-class-mapper.js'
5+
import {ZodToTypescriptReturnValueMapper} from '../language-mappers/zod-to-typescript-return-value-mapper.js'
6+
import {PythonTyping} from '../types.js'
7+
import {BaseGenerator} from './base-generator.js'
8+
9+
export class PythonGenerator extends BaseGenerator {
10+
private _typings = new Set<PythonTyping>()
11+
12+
private ENUM_IMPORT = 'from enum import StrEnum'
13+
private MUSTACHE_IMPORT = 'import pystache'
14+
private PYDANTIC_IMPORT = 'from pydantic import BaseModel'
15+
16+
get filename(): string {
17+
return 'prefab.py'
18+
}
19+
20+
generate(): string {
21+
// Need to genereate these before referencing _typings to ensure all required types are known
22+
const classTemplates = this.generateClasses()
23+
const accessorMethods = this.generateAccessorMethods()
24+
25+
const additionalDependencies = new Set<string>()
26+
27+
const typings = this._typings.size > 0 ? [...this._typings].sort().join(', ') : null
28+
29+
if (accessorMethods.length > 0) {
30+
additionalDependencies.add(this.PYDANTIC_IMPORT)
31+
}
32+
33+
if (this.configurations().some((c) => c.hasFunction)) {
34+
additionalDependencies.add(this.MUSTACHE_IMPORT)
35+
}
36+
37+
return stripIndent`
38+
# AUTOGENERATED by prefab-cli's 'gen' command
39+
import prefab_cloud_python
40+
from prefab_cloud_python import ContextDictOrContext
41+
42+
${[...additionalDependencies].join('\n ') || '# No additional dependencies required'}
43+
44+
# Optional - need to make this dynamic
45+
from datetime import timedelta # for Durations
46+
47+
${typings ? `from typing import ${typings}` : '# No additional typings required'}
48+
49+
class PrefabTypedClient:
50+
"""Client for accessing Prefab configuration with type-safe methods"""
51+
def __init__(self, client=None, use_global_client=False):
52+
"""
53+
Initialize the typed client.
54+
55+
Args:
56+
client: A Prefab client instance. If not provided and use_global_client is False,
57+
uses the global client at initialization time.
58+
use_global_client: If True, dynamically calls prefab_cloud_python.get_client() for each request
59+
instead of storing a reference. Useful in long-running applications where
60+
the client might be reset or reconfigured.
61+
"""
62+
self._prefab = prefab_cloud_python
63+
self._use_global_client = use_global_client
64+
self._client = None if use_global_client else (client or prefab_cloud_python.get_client())
65+
66+
@property
67+
def client(self):
68+
"""
69+
Returns the client to use for the current request.
70+
71+
If use_global_client is True, dynamically retrieves the current global client.
72+
Otherwise, returns the stored client instance.
73+
"""
74+
if self._use_global_client:
75+
return self._prefab.get_client()
76+
return self._client
77+
78+
${classTemplates.join('\n\n ') || '# No parameter classes generated'}
79+
80+
${
81+
// accessorMethods.join('\n\n ') ||
82+
'# No methods generated'
83+
}
84+
`
85+
}
86+
87+
private generateAccessorMethods(): string[] {
88+
const uniqueMethods: Record<string, string> = {}
89+
const schemaTypes = this.configurations().map((config) => {
90+
let methodName = camelCase(config.key)
91+
92+
// If the method name starts with a digit, prefix it with an underscore to ensure method name is valid
93+
if (/^\d/.test(methodName)) {
94+
methodName = `_${methodName}`
95+
}
96+
97+
if (uniqueMethods[methodName]) {
98+
throw new Error(
99+
`Method '${methodName}' is already registered. Prefab key ${config.key} conflicts with '${uniqueMethods[methodName]}'!`,
100+
)
101+
}
102+
103+
uniqueMethods[methodName] = config.key
104+
105+
if (config.configType === 'FEATURE_FLAG') {
106+
return stripIndent`
107+
get ${methodName}(): boolean {
108+
return this.prefab.isEnabled('${config.key}')
109+
}
110+
`
111+
}
112+
113+
if (config.hasFunction) {
114+
const returnValue = new ZodToTypescriptReturnValueMapper().resolveType(config.schema)
115+
116+
return stripIndent`
117+
${methodName}(): PrefabTypeGeneration.ReactHookConfigurationAccessor['${config.key}'] {
118+
const raw = this.get('${config.key}')
119+
return ${returnValue}
120+
}
121+
`
122+
}
123+
124+
return stripIndent`
125+
get ${methodName}(): PrefabTypeGeneration.ReactHookConfigurationAccessor['${config.key}'] {
126+
return this.get('${config.key}')
127+
}
128+
`
129+
})
130+
131+
return schemaTypes
132+
}
133+
134+
private generateClasses(): string[] {
135+
const uniqueClasses: Record<string, string> = {}
136+
137+
const schemaTypes = this.configurations().flatMap((config) => {
138+
if (config.key === 'use-mark-thing') {
139+
console.log('config', config.schema)
140+
}
141+
const mapper = new ZodToPythonClassMapper({fieldName: config.key})
142+
const results = mapper.renderClasses(config.schema)
143+
144+
results.forEach(([className, classDefinition]) => {
145+
// If it's a duplicate definition and it's not identical, bomb out
146+
if (uniqueClasses[className] && uniqueClasses[className] !== classDefinition) {
147+
throw new Error(`Different class definition with identical name ${className}`)
148+
}
149+
150+
uniqueClasses[className] = classDefinition
151+
})
152+
153+
if (mapper.hasAny) {
154+
this._typings.add(PythonTyping.Any)
155+
}
156+
if (mapper.hasTypedDict) {
157+
this._typings.add(PythonTyping.TypedDict)
158+
}
159+
if (mapper.hasOptional) {
160+
this._typings.add(PythonTyping.Optional)
161+
}
162+
if (mapper.hasTuple) {
163+
this._typings.add(PythonTyping.Tuple)
164+
}
165+
if (mapper.hasUnion) {
166+
this._typings.add(PythonTyping.Union)
167+
}
168+
169+
// only return class definitions
170+
return results.map(([, classDefinition]) => classDefinition)
171+
})
172+
173+
return schemaTypes
174+
}
175+
}

0 commit comments

Comments
 (0)