Skip to content

Commit cd9ded7

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

File tree

7 files changed

+498
-9
lines changed

7 files changed

+498
-9
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"fuzzy": "^0.1.3",
1616
"inquirer-autocomplete-standalone": "^0.8.1",
1717
"lodash.camelcase": "^4.3.0",
18+
"lodash.snakecase": "^4.1.1",
1819
"mustache": "^4.2.0",
1920
"zod": "^3.22.4"
2021
},
@@ -25,6 +26,7 @@
2526
"@types/chai": "^4",
2627
"@types/common-tags": "^1.8.4",
2728
"@types/lodash.camelcase": "^4.3.9",
29+
"@types/lodash.snakecase": "^4.1.9",
2830
"@types/mocha": "^10",
2931
"@types/mustache": "^4.2.5",
3032
"@types/node": "^18",
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import {stripIndent} from 'common-tags'
2+
import snakeCase from 'lodash.snakecase'
3+
4+
import {ZodToPythonClassMapper} from '../language-mappers/zod-to-python-class-mapper.js'
5+
import {PythonTyping} from '../types.js'
6+
import {BaseGenerator} from './base-generator.js'
7+
8+
export class PythonGenerator extends BaseGenerator {
9+
private _hasEnum = false
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+
if (this._hasEnum) {
38+
additionalDependencies.add(this.ENUM_IMPORT)
39+
}
40+
41+
return stripIndent`
42+
# AUTOGENERATED by prefab-cli's 'gen' command
43+
import prefab_cloud_python
44+
from prefab_cloud_python import ContextDictOrContext
45+
46+
${[...additionalDependencies].join('\n ') || '# No additional dependencies required'}
47+
${typings ? `from typing import ${typings}` : '# No additional typings required'}
48+
49+
# TODO: Need to make this dynamic
50+
from datetime import timedelta # for Durations
51+
${classTemplates.join('') || '# No parameter classes generated\n'}
52+
class PrefabTypedClient:
53+
"""Client for accessing Prefab configuration with type-safe methods"""
54+
def __init__(self, client=None, use_global_client=False):
55+
"""
56+
Initialize the typed client.
57+
58+
Args:
59+
client: A Prefab client instance. If not provided and use_global_client is False,
60+
uses the global client at initialization time.
61+
use_global_client: If True, dynamically calls prefab_cloud_python.get_client() for each request
62+
instead of storing a reference. Useful in long-running applications where
63+
the client might be reset or reconfigured.
64+
"""
65+
self._prefab = prefab_cloud_python
66+
self._use_global_client = use_global_client
67+
self._client = None if use_global_client else (client or prefab_cloud_python.get_client())
68+
69+
@property
70+
def client(self):
71+
"""
72+
Returns the client to use for the current request.
73+
74+
If use_global_client is True, dynamically retrieves the current global client.
75+
Otherwise, returns the stored client instance.
76+
"""
77+
if self._use_global_client:
78+
return self._prefab.get_client()
79+
return self._client
80+
81+
${accessorMethods.join(' ') || '# No methods generated'}
82+
`
83+
}
84+
85+
private generateAccessorMethods(): string[] {
86+
const uniqueMethods: Record<string, string> = {}
87+
const schemaTypes = this.configurations().map((config) => {
88+
let methodName = snakeCase(config.key)
89+
90+
// If the method name starts with a digit, prefix it with an underscore to ensure method name is valid
91+
if (/^\d/.test(methodName)) {
92+
methodName = `_${methodName}`
93+
}
94+
95+
if (uniqueMethods[methodName]) {
96+
throw new Error(
97+
`Method '${methodName}' is already registered. Prefab key ${config.key} conflicts with '${uniqueMethods[methodName]}'!`,
98+
)
99+
}
100+
101+
uniqueMethods[methodName] = config.key
102+
103+
if (config.hasFunction) {
104+
// const returnValue = new ZodToTypescriptReturnValueMapper().resolveType(config.schema)
105+
106+
return `
107+
def ${methodName}(self, context: Optional[ContextDictOrContext] = None) -> Any:
108+
return self.client.get("${config.key}", context=context)
109+
`
110+
}
111+
112+
return `
113+
def ${methodName}(self, context: Optional[ContextDictOrContext] = None) -> Any:
114+
return self.client.get("${config.key}", context=context)
115+
`
116+
})
117+
118+
return schemaTypes
119+
}
120+
121+
private generateClasses(): string[] {
122+
const uniqueClasses: Record<string, string> = {}
123+
124+
this.configurations().forEach((config) => {
125+
const mapper = new ZodToPythonClassMapper({fieldName: config.key})
126+
const results = mapper.renderClasses(config.schema)
127+
128+
results.forEach(([className, classDefinition]) => {
129+
// If it's a duplicate definition and it's not identical, bomb out
130+
if (uniqueClasses[className] && uniqueClasses[className] !== classDefinition) {
131+
throw new Error(`Different class definition with identical name ${className}`)
132+
}
133+
134+
uniqueClasses[className] = classDefinition
135+
})
136+
137+
if (mapper.hasAny) {
138+
this._typings.add(PythonTyping.Any)
139+
}
140+
if (mapper.hasTypedDict) {
141+
this._typings.add(PythonTyping.TypedDict)
142+
}
143+
if (mapper.hasOptional) {
144+
this._typings.add(PythonTyping.Optional)
145+
}
146+
if (mapper.hasTuple) {
147+
this._typings.add(PythonTyping.Tuple)
148+
}
149+
if (mapper.hasUnion) {
150+
this._typings.add(PythonTyping.Union)
151+
}
152+
if (mapper.hasEnum) {
153+
this._hasEnum = true
154+
}
155+
})
156+
157+
return Object.values(uniqueClasses)
158+
}
159+
}

0 commit comments

Comments
 (0)