Skip to content

Commit ff3d3a0

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

5 files changed

Lines changed: 504 additions & 9 deletions

File tree

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

0 commit comments

Comments
 (0)