@modularizer/plat-client 0.4.0

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.
Files changed (233) hide show
  1. package/dist/args/coerce.d.ts +54 -0
  2. package/dist/args/coerce.d.ts.map +1 -0
  3. package/dist/args/coerce.js +236 -0
  4. package/dist/args/coerce.js.map +1 -0
  5. package/dist/args/index.d.ts +2 -0
  6. package/dist/args/index.d.ts.map +1 -0
  7. package/dist/args/index.js +2 -0
  8. package/dist/args/index.js.map +1 -0
  9. package/dist/args/scalars.d.ts +87 -0
  10. package/dist/args/scalars.d.ts.map +1 -0
  11. package/dist/args/scalars.js +22 -0
  12. package/dist/args/scalars.js.map +1 -0
  13. package/dist/args/validate.d.ts +23 -0
  14. package/dist/args/validate.d.ts.map +1 -0
  15. package/dist/args/validate.js +185 -0
  16. package/dist/args/validate.js.map +1 -0
  17. package/dist/args/z2.d.ts +27 -0
  18. package/dist/args/z2.d.ts.map +1 -0
  19. package/dist/args/z2.js +24 -0
  20. package/dist/args/z2.js.map +1 -0
  21. package/dist/client/css-transport-plugin.d.ts +19 -0
  22. package/dist/client/css-transport-plugin.d.ts.map +1 -0
  23. package/dist/client/css-transport-plugin.js +78 -0
  24. package/dist/client/css-transport-plugin.js.map +1 -0
  25. package/dist/client/file-transport-plugin.d.ts +28 -0
  26. package/dist/client/file-transport-plugin.d.ts.map +1 -0
  27. package/dist/client/file-transport-plugin.js +80 -0
  28. package/dist/client/file-transport-plugin.js.map +1 -0
  29. package/dist/client/http-transport-plugin.d.ts +27 -0
  30. package/dist/client/http-transport-plugin.d.ts.map +1 -0
  31. package/dist/client/http-transport-plugin.js +48 -0
  32. package/dist/client/http-transport-plugin.js.map +1 -0
  33. package/dist/client/index.d.ts +7 -0
  34. package/dist/client/index.d.ts.map +1 -0
  35. package/dist/client/index.js +7 -0
  36. package/dist/client/index.js.map +1 -0
  37. package/dist/client/openapi-client.d.ts +334 -0
  38. package/dist/client/openapi-client.d.ts.map +1 -0
  39. package/dist/client/openapi-client.js +910 -0
  40. package/dist/client/openapi-client.js.map +1 -0
  41. package/dist/client/proxy.d.ts +5 -0
  42. package/dist/client/proxy.d.ts.map +1 -0
  43. package/dist/client/proxy.js +353 -0
  44. package/dist/client/proxy.js.map +1 -0
  45. package/dist/client/request-builder.d.ts +5 -0
  46. package/dist/client/request-builder.d.ts.map +1 -0
  47. package/dist/client/request-builder.js +88 -0
  48. package/dist/client/request-builder.js.map +1 -0
  49. package/dist/client/rpc-transport-plugin.d.ts +17 -0
  50. package/dist/client/rpc-transport-plugin.d.ts.map +1 -0
  51. package/dist/client/rpc-transport-plugin.js +69 -0
  52. package/dist/client/rpc-transport-plugin.js.map +1 -0
  53. package/dist/client/tools.d.ts +69 -0
  54. package/dist/client/tools.d.ts.map +1 -0
  55. package/dist/client/tools.js +122 -0
  56. package/dist/client/tools.js.map +1 -0
  57. package/dist/client/transport-plugin.d.ts +62 -0
  58. package/dist/client/transport-plugin.d.ts.map +1 -0
  59. package/dist/client/transport-plugin.js +40 -0
  60. package/dist/client/transport-plugin.js.map +1 -0
  61. package/dist/client-entry.d.ts +25 -0
  62. package/dist/client-entry.d.ts.map +1 -0
  63. package/dist/client-entry.js +25 -0
  64. package/dist/client-entry.js.map +1 -0
  65. package/dist/client-server-entry.d.ts +13 -0
  66. package/dist/client-server-entry.d.ts.map +1 -0
  67. package/dist/client-server-entry.js +13 -0
  68. package/dist/client-server-entry.js.map +1 -0
  69. package/dist/client-side-python/runtime.d.ts +102 -0
  70. package/dist/client-side-python/runtime.d.ts.map +1 -0
  71. package/dist/client-side-python/runtime.js +595 -0
  72. package/dist/client-side-python/runtime.js.map +1 -0
  73. package/dist/client-side-server/bootstrap.d.ts +3 -0
  74. package/dist/client-side-server/bootstrap.d.ts.map +1 -0
  75. package/dist/client-side-server/bootstrap.js +20 -0
  76. package/dist/client-side-server/bootstrap.js.map +1 -0
  77. package/dist/client-side-server/channel.d.ts +17 -0
  78. package/dist/client-side-server/channel.d.ts.map +1 -0
  79. package/dist/client-side-server/channel.js +38 -0
  80. package/dist/client-side-server/channel.js.map +1 -0
  81. package/dist/client-side-server/identity.d.ts +116 -0
  82. package/dist/client-side-server/identity.d.ts.map +1 -0
  83. package/dist/client-side-server/identity.js +358 -0
  84. package/dist/client-side-server/identity.js.map +1 -0
  85. package/dist/client-side-server/mqtt-webrtc.d.ts +77 -0
  86. package/dist/client-side-server/mqtt-webrtc.d.ts.map +1 -0
  87. package/dist/client-side-server/mqtt-webrtc.js +575 -0
  88. package/dist/client-side-server/mqtt-webrtc.js.map +1 -0
  89. package/dist/client-side-server/protocol.d.ts +49 -0
  90. package/dist/client-side-server/protocol.d.ts.map +1 -0
  91. package/dist/client-side-server/protocol.js +13 -0
  92. package/dist/client-side-server/protocol.js.map +1 -0
  93. package/dist/client-side-server/runtime.d.ts +57 -0
  94. package/dist/client-side-server/runtime.d.ts.map +1 -0
  95. package/dist/client-side-server/runtime.js +188 -0
  96. package/dist/client-side-server/runtime.js.map +1 -0
  97. package/dist/client-side-server/server.d.ts +75 -0
  98. package/dist/client-side-server/server.d.ts.map +1 -0
  99. package/dist/client-side-server/server.js +380 -0
  100. package/dist/client-side-server/server.js.map +1 -0
  101. package/dist/client-side-server/signaling.d.ts +10 -0
  102. package/dist/client-side-server/signaling.d.ts.map +1 -0
  103. package/dist/client-side-server/signaling.js +19 -0
  104. package/dist/client-side-server/signaling.js.map +1 -0
  105. package/dist/client-side-server/source-analysis.d.ts +29 -0
  106. package/dist/client-side-server/source-analysis.d.ts.map +1 -0
  107. package/dist/client-side-server/source-analysis.js +395 -0
  108. package/dist/client-side-server/source-analysis.js.map +1 -0
  109. package/dist/generated/python-browser-sources.d.ts +2 -0
  110. package/dist/generated/python-browser-sources.d.ts.map +1 -0
  111. package/dist/generated/python-browser-sources.js +13 -0
  112. package/dist/generated/python-browser-sources.js.map +1 -0
  113. package/dist/logging.d.ts +9 -0
  114. package/dist/logging.d.ts.map +1 -0
  115. package/dist/logging.js +64 -0
  116. package/dist/logging.js.map +1 -0
  117. package/dist/python-browser-entry.d.ts +2 -0
  118. package/dist/python-browser-entry.d.ts.map +1 -0
  119. package/dist/python-browser-entry.js +2 -0
  120. package/dist/python-browser-entry.js.map +1 -0
  121. package/dist/rpc.d.ts +39 -0
  122. package/dist/rpc.d.ts.map +1 -0
  123. package/dist/rpc.js +2 -0
  124. package/dist/rpc.js.map +1 -0
  125. package/dist/server/authority-server.d.ts +27 -0
  126. package/dist/server/authority-server.d.ts.map +1 -0
  127. package/dist/server/authority-server.js +97 -0
  128. package/dist/server/authority-server.js.map +1 -0
  129. package/dist/server/cache/index.d.ts +2 -0
  130. package/dist/server/cache/index.d.ts.map +1 -0
  131. package/dist/server/cache/index.js +2 -0
  132. package/dist/server/cache/index.js.map +1 -0
  133. package/dist/server/cache/utils.d.ts +30 -0
  134. package/dist/server/cache/utils.d.ts.map +1 -0
  135. package/dist/server/cache/utils.js +116 -0
  136. package/dist/server/cache/utils.js.map +1 -0
  137. package/dist/server/core.d.ts +43 -0
  138. package/dist/server/core.d.ts.map +1 -0
  139. package/dist/server/core.js +215 -0
  140. package/dist/server/core.js.map +1 -0
  141. package/dist/server/operation-registry.d.ts +9 -0
  142. package/dist/server/operation-registry.d.ts.map +1 -0
  143. package/dist/server/operation-registry.js +16 -0
  144. package/dist/server/operation-registry.js.map +1 -0
  145. package/dist/server/param-aliases.d.ts +40 -0
  146. package/dist/server/param-aliases.d.ts.map +1 -0
  147. package/dist/server/param-aliases.js +112 -0
  148. package/dist/server/param-aliases.js.map +1 -0
  149. package/dist/server/rate-limit/index.d.ts +2 -0
  150. package/dist/server/rate-limit/index.d.ts.map +1 -0
  151. package/dist/server/rate-limit/index.js +2 -0
  152. package/dist/server/rate-limit/index.js.map +1 -0
  153. package/dist/server/rate-limit/utils.d.ts +27 -0
  154. package/dist/server/rate-limit/utils.d.ts.map +1 -0
  155. package/dist/server/rate-limit/utils.js +126 -0
  156. package/dist/server/rate-limit/utils.js.map +1 -0
  157. package/dist/server/routing.d.ts +39 -0
  158. package/dist/server/routing.d.ts.map +1 -0
  159. package/dist/server/routing.js +70 -0
  160. package/dist/server/routing.js.map +1 -0
  161. package/dist/server/token-limit/index.d.ts +2 -0
  162. package/dist/server/token-limit/index.d.ts.map +1 -0
  163. package/dist/server/token-limit/index.js +2 -0
  164. package/dist/server/token-limit/index.js.map +1 -0
  165. package/dist/server/token-limit/utils.d.ts +44 -0
  166. package/dist/server/token-limit/utils.d.ts.map +1 -0
  167. package/dist/server/token-limit/utils.js +260 -0
  168. package/dist/server/token-limit/utils.js.map +1 -0
  169. package/dist/server/tools.d.ts +33 -0
  170. package/dist/server/tools.d.ts.map +1 -0
  171. package/dist/server/tools.js +160 -0
  172. package/dist/server/tools.js.map +1 -0
  173. package/dist/server/transports.d.ts +25 -0
  174. package/dist/server/transports.d.ts.map +1 -0
  175. package/dist/server/transports.js +2 -0
  176. package/dist/server/transports.js.map +1 -0
  177. package/dist/shared/tools.d.ts +24 -0
  178. package/dist/shared/tools.d.ts.map +1 -0
  179. package/dist/shared/tools.js +86 -0
  180. package/dist/shared/tools.js.map +1 -0
  181. package/dist/spec/decorators.d.ts +41 -0
  182. package/dist/spec/decorators.d.ts.map +1 -0
  183. package/dist/spec/decorators.js +93 -0
  184. package/dist/spec/decorators.js.map +1 -0
  185. package/dist/spec/index.d.ts +3 -0
  186. package/dist/spec/index.d.ts.map +1 -0
  187. package/dist/spec/index.js +3 -0
  188. package/dist/spec/index.js.map +1 -0
  189. package/dist/spec/metadata.d.ts +5 -0
  190. package/dist/spec/metadata.d.ts.map +1 -0
  191. package/dist/spec/metadata.js +37 -0
  192. package/dist/spec/metadata.js.map +1 -0
  193. package/dist/types/client-route.d.ts +7 -0
  194. package/dist/types/client-route.d.ts.map +1 -0
  195. package/dist/types/client-route.js +2 -0
  196. package/dist/types/client-route.js.map +1 -0
  197. package/dist/types/client.d.ts +81 -0
  198. package/dist/types/client.d.ts.map +1 -0
  199. package/dist/types/client.js +14 -0
  200. package/dist/types/client.js.map +1 -0
  201. package/dist/types/endpoints.d.ts +76 -0
  202. package/dist/types/endpoints.d.ts.map +1 -0
  203. package/dist/types/endpoints.js +2 -0
  204. package/dist/types/endpoints.js.map +1 -0
  205. package/dist/types/errors.d.ts +86 -0
  206. package/dist/types/errors.d.ts.map +1 -0
  207. package/dist/types/errors.js +153 -0
  208. package/dist/types/errors.js.map +1 -0
  209. package/dist/types/http.d.ts +80 -0
  210. package/dist/types/http.d.ts.map +1 -0
  211. package/dist/types/http.js +61 -0
  212. package/dist/types/http.js.map +1 -0
  213. package/dist/types/index.d.ts +10 -0
  214. package/dist/types/index.d.ts.map +1 -0
  215. package/dist/types/index.js +10 -0
  216. package/dist/types/index.js.map +1 -0
  217. package/dist/types/openapi.d.ts +220 -0
  218. package/dist/types/openapi.d.ts.map +1 -0
  219. package/dist/types/openapi.js +11 -0
  220. package/dist/types/openapi.js.map +1 -0
  221. package/dist/types/opts.d.ts +46 -0
  222. package/dist/types/opts.d.ts.map +1 -0
  223. package/dist/types/opts.js +2 -0
  224. package/dist/types/opts.js.map +1 -0
  225. package/dist/types/plugins.d.ts +93 -0
  226. package/dist/types/plugins.d.ts.map +1 -0
  227. package/dist/types/plugins.js +5 -0
  228. package/dist/types/plugins.js.map +1 -0
  229. package/dist/types/tools.d.ts +52 -0
  230. package/dist/types/tools.d.ts.map +1 -0
  231. package/dist/types/tools.js +2 -0
  232. package/dist/types/tools.js.map +1 -0
  233. package/package.json +51 -0
@@ -0,0 +1,395 @@
1
+ export function analyzeClientSideServerSource(ts, source, options = {}) {
2
+ const sourceFile = ts.createSourceFile('client-side-server.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
3
+ const interfaces = new Map();
4
+ const typeAliases = new Map();
5
+ const enums = new Map();
6
+ const controllers = [];
7
+ const undecoratedMode = options.undecoratedMode ?? 'POST';
8
+ ts.forEachChild(sourceFile, (node) => {
9
+ if (isKind(ts, node, 'InterfaceDeclaration') && node.name?.text) {
10
+ interfaces.set(node.name.text, node);
11
+ return;
12
+ }
13
+ if (isKind(ts, node, 'TypeAliasDeclaration') && node.name?.text) {
14
+ typeAliases.set(node.name.text, node.type);
15
+ return;
16
+ }
17
+ if (isKind(ts, node, 'EnumDeclaration') && node.name?.text) {
18
+ enums.set(node.name.text, node);
19
+ return;
20
+ }
21
+ if (isKind(ts, node, 'ClassDeclaration') && node.name?.text) {
22
+ controllers.push(analyzeController(ts, node, interfaces, typeAliases, enums, undecoratedMode));
23
+ }
24
+ });
25
+ return { controllers };
26
+ }
27
+ function analyzeController(ts, classNode, interfaces, typeAliases, enums, undecoratedMode) {
28
+ const methods = (classNode.members ?? [])
29
+ .filter((member) => isMethodExposed(ts, member, undecoratedMode))
30
+ .map((method) => analyzeMethod(ts, method, interfaces, typeAliases, enums));
31
+ return {
32
+ name: classNode.name.text,
33
+ methods,
34
+ };
35
+ }
36
+ function analyzeMethod(ts, methodNode, interfaces, typeAliases, enums) {
37
+ const docs = getNodeDoc(ts, methodNode);
38
+ const inputParam = (methodNode.parameters ?? [])[0];
39
+ const inputSchema = inputParam?.type
40
+ ? typeNodeToSchema(ts, inputParam.type, interfaces, typeAliases, enums)
41
+ : undefined;
42
+ const explicitReturnType = unwrapPromiseType(ts, methodNode.type);
43
+ const outputSchema = explicitReturnType
44
+ ? typeNodeToSchema(ts, explicitReturnType, interfaces, typeAliases, enums)
45
+ : inferMethodReturnSchema(ts, methodNode, interfaces, typeAliases, enums);
46
+ return {
47
+ name: methodNode.name.getText ? methodNode.name.getText() : methodNode.name.text,
48
+ summary: docs.summary,
49
+ description: docs.description,
50
+ inputSchema: normalizeObjectSchema(inputSchema),
51
+ outputSchema,
52
+ };
53
+ }
54
+ function isMethodExposed(ts, member, undecoratedMode) {
55
+ if (!isKind(ts, member, 'MethodDeclaration'))
56
+ return false;
57
+ const name = member.name?.getText?.() ?? member.name?.text;
58
+ if (!name || name === 'constructor' || String(name).startsWith('_'))
59
+ return false;
60
+ return Boolean(getHttpDecorator(ts, member)) || undecoratedMode !== 'private';
61
+ }
62
+ function getNodeDoc(_ts, node) {
63
+ const blocks = Array.isArray(node?.jsDoc) ? node.jsDoc : [];
64
+ if (blocks.length === 0)
65
+ return {};
66
+ const raw = blocks
67
+ .map((block) => {
68
+ if (typeof block.comment === 'string')
69
+ return block.comment;
70
+ if (Array.isArray(block.comment)) {
71
+ return block.comment.map((part) => part?.text ?? '').join('');
72
+ }
73
+ return '';
74
+ })
75
+ .join('\n')
76
+ .trim();
77
+ if (!raw)
78
+ return {};
79
+ const paragraphs = raw
80
+ .split(/\n\s*\n/)
81
+ .map((part) => part.trim())
82
+ .filter(Boolean);
83
+ if (paragraphs.length === 0)
84
+ return {};
85
+ return {
86
+ summary: paragraphs[0],
87
+ description: paragraphs.join('\n\n'),
88
+ };
89
+ }
90
+ function inferMethodReturnSchema(ts, methodNode, interfaces, typeAliases, enums) {
91
+ if (!methodNode.body)
92
+ return undefined;
93
+ const returns = [];
94
+ walk(ts, methodNode.body, (node) => {
95
+ if (isKind(ts, node, 'ReturnStatement') && node.expression) {
96
+ const schema = expressionToSchema(ts, node.expression, interfaces, typeAliases, enums);
97
+ if (schema)
98
+ returns.push(schema);
99
+ }
100
+ });
101
+ if (returns.length === 0)
102
+ return undefined;
103
+ if (returns.length === 1)
104
+ return returns[0];
105
+ return mergeSchemas(returns);
106
+ }
107
+ function expressionToSchema(ts, expression, interfaces, typeAliases, enums) {
108
+ if (isKind(ts, expression, 'ObjectLiteralExpression')) {
109
+ const properties = {};
110
+ const required = [];
111
+ for (const prop of expression.properties ?? []) {
112
+ if (isKind(ts, prop, 'PropertyAssignment')) {
113
+ const name = propertyNameText(prop.name);
114
+ if (!name)
115
+ continue;
116
+ const schema = expressionToSchema(ts, prop.initializer, interfaces, typeAliases, enums) ?? {};
117
+ properties[name] = schema;
118
+ required.push(name);
119
+ }
120
+ }
121
+ return {
122
+ type: 'object',
123
+ properties,
124
+ required,
125
+ };
126
+ }
127
+ if (isKind(ts, expression, 'ArrayLiteralExpression')) {
128
+ const items = (expression.elements ?? [])
129
+ .map((element) => expressionToSchema(ts, element, interfaces, typeAliases, enums))
130
+ .filter(Boolean);
131
+ return {
132
+ type: 'array',
133
+ items: items.length <= 1 ? (items[0] ?? {}) : { oneOf: items },
134
+ };
135
+ }
136
+ if (isKind(ts, expression, 'StringLiteral') || isKind(ts, expression, 'NoSubstitutionTemplateLiteral')) {
137
+ return { type: 'string' };
138
+ }
139
+ if (isKind(ts, expression, 'NumericLiteral')) {
140
+ return { type: 'number' };
141
+ }
142
+ if (isKind(ts, expression, 'TrueKeyword') || isKind(ts, expression, 'FalseKeyword')) {
143
+ return { type: 'boolean' };
144
+ }
145
+ if (isKind(ts, expression, 'AsExpression') || isKind(ts, expression, 'ParenthesizedExpression')) {
146
+ return expressionToSchema(ts, expression.expression, interfaces, typeAliases, enums);
147
+ }
148
+ if (expression.kind === ts.SyntaxKind.NullKeyword) {
149
+ return { type: 'null' };
150
+ }
151
+ return undefined;
152
+ }
153
+ function typeNodeToSchema(ts, typeNode, interfaces, typeAliases, enums, seen = new Set()) {
154
+ if (!typeNode)
155
+ return undefined;
156
+ if (isKind(ts, typeNode, 'ParenthesizedType')) {
157
+ return typeNodeToSchema(ts, typeNode.type, interfaces, typeAliases, enums, seen);
158
+ }
159
+ if (isKind(ts, typeNode, 'TypeLiteral')) {
160
+ return membersToObjectSchema(ts, typeNode.members ?? [], interfaces, typeAliases, enums, seen);
161
+ }
162
+ if (isKind(ts, typeNode, 'ArrayType')) {
163
+ return {
164
+ type: 'array',
165
+ items: typeNodeToSchema(ts, typeNode.elementType, interfaces, typeAliases, enums, seen) ?? {},
166
+ };
167
+ }
168
+ if (isKind(ts, typeNode, 'TupleType')) {
169
+ return {
170
+ type: 'array',
171
+ prefixItems: (typeNode.elements ?? []).map((element) => typeNodeToSchema(ts, element, interfaces, typeAliases, enums, seen) ?? {}),
172
+ };
173
+ }
174
+ if (isKind(ts, typeNode, 'UnionType')) {
175
+ const memberSchemas = (typeNode.types ?? [])
176
+ .map((member) => typeNodeToSchema(ts, member, interfaces, typeAliases, enums, new Set(seen)))
177
+ .filter(Boolean);
178
+ const literalValues = memberSchemas
179
+ .map((schema) => Object.prototype.hasOwnProperty.call(schema, 'const') ? schema.const : undefined)
180
+ .filter((value) => value !== undefined);
181
+ if (literalValues.length === memberSchemas.length && literalValues.length > 0) {
182
+ return {
183
+ type: typeof literalValues[0] === 'number' ? 'number' : typeof literalValues[0] === 'boolean' ? 'boolean' : 'string',
184
+ enum: literalValues,
185
+ };
186
+ }
187
+ return { oneOf: memberSchemas };
188
+ }
189
+ if (isKind(ts, typeNode, 'LiteralType')) {
190
+ const literal = literalNodeToValue(ts, typeNode.literal);
191
+ if (literal === undefined)
192
+ return undefined;
193
+ return {
194
+ type: typeof literal === 'number' ? 'number' : typeof literal === 'boolean' ? 'boolean' : 'string',
195
+ const: literal,
196
+ };
197
+ }
198
+ if (isKind(ts, typeNode, 'TypeReference')) {
199
+ const typeName = typeNode.typeName?.getText?.() ?? typeNode.typeName?.text;
200
+ if (!typeName)
201
+ return undefined;
202
+ if (typeName === 'Promise') {
203
+ return typeNodeToSchema(ts, typeNode.typeArguments?.[0], interfaces, typeAliases, enums, seen);
204
+ }
205
+ if (typeName === 'Array') {
206
+ return {
207
+ type: 'array',
208
+ items: typeNodeToSchema(ts, typeNode.typeArguments?.[0], interfaces, typeAliases, enums, seen) ?? {},
209
+ };
210
+ }
211
+ if (typeName === 'Record') {
212
+ return {
213
+ type: 'object',
214
+ additionalProperties: typeNodeToSchema(ts, typeNode.typeArguments?.[1], interfaces, typeAliases, enums, seen) ?? {},
215
+ };
216
+ }
217
+ if (typeName === 'Date') {
218
+ return { type: 'string', format: 'date-time' };
219
+ }
220
+ if (seen.has(typeName)) {
221
+ return { type: 'object' };
222
+ }
223
+ seen.add(typeName);
224
+ if (interfaces.has(typeName)) {
225
+ const schema = membersToObjectSchema(ts, interfaces.get(typeName).members ?? [], interfaces, typeAliases, enums, seen);
226
+ seen.delete(typeName);
227
+ return schema;
228
+ }
229
+ if (typeAliases.has(typeName)) {
230
+ const schema = typeNodeToSchema(ts, typeAliases.get(typeName), interfaces, typeAliases, enums, seen);
231
+ seen.delete(typeName);
232
+ return schema;
233
+ }
234
+ if (enums.has(typeName)) {
235
+ const schema = enumToSchema(ts, enums.get(typeName));
236
+ seen.delete(typeName);
237
+ return schema;
238
+ }
239
+ seen.delete(typeName);
240
+ return { type: 'object' };
241
+ }
242
+ switch (typeNode.kind) {
243
+ case ts.SyntaxKind.StringKeyword:
244
+ return { type: 'string' };
245
+ case ts.SyntaxKind.NumberKeyword:
246
+ return { type: 'number' };
247
+ case ts.SyntaxKind.BooleanKeyword:
248
+ return { type: 'boolean' };
249
+ case ts.SyntaxKind.NullKeyword:
250
+ return { type: 'null' };
251
+ case ts.SyntaxKind.AnyKeyword:
252
+ case ts.SyntaxKind.UnknownKeyword:
253
+ return {};
254
+ case ts.SyntaxKind.VoidKeyword:
255
+ return undefined;
256
+ default:
257
+ return { type: 'object' };
258
+ }
259
+ }
260
+ function membersToObjectSchema(ts, members, interfaces, typeAliases, enums, seen) {
261
+ const properties = {};
262
+ const required = [];
263
+ for (const member of members) {
264
+ if (!isKind(ts, member, 'PropertySignature') && !isKind(ts, member, 'PropertyDeclaration')) {
265
+ continue;
266
+ }
267
+ const name = propertyNameText(member.name);
268
+ if (!name)
269
+ continue;
270
+ const schema = typeNodeToSchema(ts, member.type, interfaces, typeAliases, enums, new Set(seen)) ?? {};
271
+ properties[name] = schema;
272
+ if (!member.questionToken) {
273
+ required.push(name);
274
+ }
275
+ }
276
+ return {
277
+ type: 'object',
278
+ properties,
279
+ required,
280
+ };
281
+ }
282
+ function enumToSchema(ts, enumNode) {
283
+ const values = (enumNode.members ?? [])
284
+ .map((member) => {
285
+ if (member.initializer) {
286
+ return literalNodeToValue(ts, member.initializer);
287
+ }
288
+ return member.name?.text;
289
+ })
290
+ .filter((value) => value !== undefined);
291
+ return {
292
+ type: values.every((value) => typeof value === 'number') ? 'number' : 'string',
293
+ enum: values,
294
+ };
295
+ }
296
+ function unwrapPromiseType(ts, typeNode) {
297
+ if (!typeNode || !isKind(ts, typeNode, 'TypeReference'))
298
+ return typeNode;
299
+ const typeName = typeNode.typeName?.getText?.() ?? typeNode.typeName?.text;
300
+ if (typeName === 'Promise' && typeNode.typeArguments?.length) {
301
+ return typeNode.typeArguments[0];
302
+ }
303
+ return typeNode;
304
+ }
305
+ function hasDecorator(ts, node, name) {
306
+ return getDecorators(ts, node).some((decorator) => decoratorName(decorator) === name);
307
+ }
308
+ function getHttpDecorator(ts, node) {
309
+ return getDecorators(ts, node)
310
+ .map((decorator) => decoratorName(decorator))
311
+ .find((name) => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(name ?? ''));
312
+ }
313
+ function getDecorators(ts, node) {
314
+ if (typeof ts.canHaveDecorators === 'function' && typeof ts.getDecorators === 'function' && ts.canHaveDecorators(node)) {
315
+ return [...(ts.getDecorators(node) ?? [])];
316
+ }
317
+ return [...(node.decorators ?? [])];
318
+ }
319
+ function decoratorName(decorator) {
320
+ const expression = decorator.expression ?? decorator;
321
+ if (!expression)
322
+ return undefined;
323
+ if (expression.expression?.text)
324
+ return expression.expression.text;
325
+ if (expression.expression?.getText)
326
+ return expression.expression.getText();
327
+ if (expression.text)
328
+ return expression.text;
329
+ if (expression.getText)
330
+ return expression.getText();
331
+ return undefined;
332
+ }
333
+ function propertyNameText(nameNode) {
334
+ if (!nameNode)
335
+ return undefined;
336
+ if (typeof nameNode.text === 'string')
337
+ return nameNode.text;
338
+ if (typeof nameNode.getText === 'function') {
339
+ return nameNode.getText().replace(/^['"]|['"]$/g, '');
340
+ }
341
+ return undefined;
342
+ }
343
+ function literalNodeToValue(ts, node) {
344
+ if (!node)
345
+ return undefined;
346
+ if (isKind(ts, node, 'StringLiteral') || isKind(ts, node, 'NoSubstitutionTemplateLiteral')) {
347
+ return node.text;
348
+ }
349
+ if (isKind(ts, node, 'NumericLiteral')) {
350
+ return Number(node.text);
351
+ }
352
+ if (isKind(ts, node, 'TrueKeyword'))
353
+ return true;
354
+ if (isKind(ts, node, 'FalseKeyword'))
355
+ return false;
356
+ return undefined;
357
+ }
358
+ function mergeSchemas(schemas) {
359
+ if (schemas.every((schema) => schema.type === 'object' && schema.properties)) {
360
+ const properties = {};
361
+ const presence = new Map();
362
+ for (const schema of schemas) {
363
+ for (const [name, propertySchema] of Object.entries(schema.properties ?? {})) {
364
+ properties[name] = propertySchema;
365
+ presence.set(name, (presence.get(name) ?? 0) + 1);
366
+ }
367
+ }
368
+ const required = Array.from(presence.entries())
369
+ .filter(([, count]) => count === schemas.length)
370
+ .map(([name]) => name);
371
+ return { type: 'object', properties, required };
372
+ }
373
+ return { oneOf: schemas };
374
+ }
375
+ function normalizeObjectSchema(schema) {
376
+ if (!schema)
377
+ return undefined;
378
+ if (schema.type === 'object') {
379
+ return {
380
+ type: 'object',
381
+ properties: schema.properties ?? {},
382
+ required: schema.required ?? [],
383
+ ...(schema.additionalProperties !== undefined ? { additionalProperties: schema.additionalProperties } : {}),
384
+ };
385
+ }
386
+ return schema;
387
+ }
388
+ function isKind(ts, node, kindName) {
389
+ return Boolean(node && node.kind === ts.SyntaxKind[kindName]);
390
+ }
391
+ function walk(ts, node, visitor) {
392
+ visitor(node);
393
+ ts.forEachChild(node, (child) => walk(ts, child, visitor));
394
+ }
395
+ //# sourceMappingURL=source-analysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-analysis.js","sourceRoot":"","sources":["../../../typescript/src/client-side-server/source-analysis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const PYTHON_BROWSER_SOURCES: Record<string, string>;
2
+ //# sourceMappingURL=python-browser-sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"python-browser-sources.d.ts","sourceRoot":"","sources":["../../../typescript/src/generated/python-browser-sources.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUzD,CAAA"}
@@ -0,0 +1,13 @@
1
+ // Auto-generated by scripts/gen-python-browser-sources.ts
2
+ export const PYTHON_BROWSER_SOURCES = {
3
+ "plat_browser/__init__.py": "from .client import BrowserPLATClient, connect_client_side_server, run_python_client_source\nfrom .decorators import Controller, DELETE, GET, PATCH, POST, PUT\nfrom .errors import HttpError, HttpResponse\nfrom .plugins import BucketConfig\nfrom .server import (\n BrowserPackagePlan,\n BrowserPLATServer,\n BrowserServerDefinition,\n create_browser_server,\n prepare_python_source,\n serve_client_side_server,\n)\nfrom .types import RouteContext\n\n__all__ = [\n \"Controller\",\n \"DELETE\",\n \"GET\",\n \"PATCH\",\n \"POST\",\n \"PUT\",\n \"BrowserPLATClient\",\n \"BrowserPackagePlan\",\n \"BrowserPLATServer\",\n \"BrowserServerDefinition\",\n \"BucketConfig\",\n \"HttpError\",\n \"HttpResponse\",\n \"RouteContext\",\n \"connect_client_side_server\",\n \"create_browser_server\",\n \"prepare_python_source\",\n \"run_python_client_source\",\n \"serve_client_side_server\",\n]\n",
4
+ "plat_browser/client.py": "from __future__ import annotations\n\nimport ast\nfrom collections.abc import Awaitable, Callable\nfrom typing import Any\n\n\nBrowserClientConnectBridge = Callable[[str, Any], Awaitable[Any]]\nBrowserClientCallBridge = Callable[[int, str, Any], Awaitable[Any]]\n\n_CONNECT_BRIDGE: BrowserClientConnectBridge | None = None\n_CALL_BRIDGE: BrowserClientCallBridge | None = None\n\n\nclass BrowserPLATClient:\n def __init__(self, client_id: int, base_url: str, openapi: Any = None) -> None:\n self.client_id = client_id\n self.base_url = base_url\n self.openapi = openapi\n\n async def call(self, method_name: str, input: Any = None, /, **kwargs: Any) -> Any:\n call_bridge = _require_call_bridge()\n payload = _merge_input(input, kwargs)\n result = await call_bridge(self.client_id, method_name, payload)\n return _to_python_value(result)\n\n def __getattr__(self, method_name: str):\n async def invoke(input: Any = None, /, **kwargs: Any) -> Any:\n return await self.call(method_name, input, **kwargs)\n\n return invoke\n\n\ndef _set_browser_client_bridge(\n connect_bridge: BrowserClientConnectBridge,\n call_bridge: BrowserClientCallBridge,\n) -> None:\n global _CONNECT_BRIDGE, _CALL_BRIDGE\n _CONNECT_BRIDGE = connect_bridge\n _CALL_BRIDGE = call_bridge\n\n\nasync def connect_client_side_server(base_url: str, options: Any = None) -> BrowserPLATClient:\n connect_bridge = _require_connect_bridge()\n connection = _to_python_value(await connect_bridge(base_url, options))\n if not isinstance(connection, dict):\n raise RuntimeError(\"Browser client bridge returned an invalid connection payload.\")\n return BrowserPLATClient(\n client_id=int(connection[\"client_id\"]),\n base_url=str(connection.get(\"base_url\") or base_url),\n openapi=connection.get(\"openapi\"),\n )\n\n\nasync def run_python_client_source(source: str) -> Any:\n namespace = {\n \"__name__\": \"__plat_browser_client__\",\n \"connect_client_side_server\": connect_client_side_server,\n }\n compiled = _compile_python_client_snippet(source)\n exec(compiled, namespace)\n result = await namespace[\"__plat_browser_client_main__\"]()\n return _to_python_value(result)\n\n\ndef _compile_python_client_snippet(source: str):\n module = ast.parse(source, mode=\"exec\")\n body = list(module.body)\n if body and isinstance(body[-1], ast.Expr):\n body[-1] = ast.Return(body[-1].value)\n else:\n body.append(ast.Return(value=ast.Constant(value=None)))\n function = ast.AsyncFunctionDef(\n name=\"__plat_browser_client_main__\",\n args=ast.arguments(\n posonlyargs=[],\n args=[],\n kwonlyargs=[],\n kw_defaults=[],\n defaults=[],\n vararg=None,\n kwarg=None,\n ),\n body=body,\n decorator_list=[],\n returns=None,\n type_comment=None,\n )\n wrapper = ast.Module(body=[function], type_ignores=[])\n ast.fix_missing_locations(wrapper)\n return compile(wrapper, \"<plat_browser_client>\", \"exec\")\n\n\ndef _merge_input(input_data: Any, kwargs: dict[str, Any]) -> Any:\n if input_data is None:\n return kwargs or {}\n if kwargs:\n if isinstance(input_data, dict):\n merged = dict(input_data)\n merged.update(kwargs)\n return merged\n raise TypeError(\"Cannot pass both a non-dict positional input and keyword arguments.\")\n return input_data\n\n\ndef _to_python_value(value: Any) -> Any:\n to_py = getattr(value, \"to_py\", None)\n if callable(to_py):\n try:\n return to_py()\n except TypeError:\n return to_py(depth=-1)\n return value\n\n\ndef _require_connect_bridge() -> BrowserClientConnectBridge:\n if _CONNECT_BRIDGE is None:\n raise RuntimeError(\"Browser Python client bridge is not installed in this runtime.\")\n return _CONNECT_BRIDGE\n\n\ndef _require_call_bridge() -> BrowserClientCallBridge:\n if _CALL_BRIDGE is None:\n raise RuntimeError(\"Browser Python client bridge is not installed in this runtime.\")\n return _CALL_BRIDGE\n",
5
+ "plat_browser/decorators.py": "from __future__ import annotations\n\nfrom collections.abc import Callable, Mapping\nfrom typing import Any, TypeVar, cast\n\nfrom .metadata import ensure_controller_meta\nfrom .types import ControllerMeta, HttpMethod, RouteMeta\n\nF = TypeVar(\"F\", bound=Callable[..., Any])\nROUTE_METADATA_KEY = \"__plat_browser_route_meta__\"\n\n\ndef Controller(\n controller_name: str | None = None,\n opts: Mapping[str, Any] | None = None,\n):\n options = dict(opts or {})\n\n def decorator(cls: type) -> type:\n meta = ensure_controller_meta(cls)\n _apply_controller_options(meta, cls, controller_name, options)\n return cls\n\n return decorator\n\n\ndef _apply_controller_options(\n meta: ControllerMeta,\n cls: type,\n controller_name: str | None,\n options: dict[str, Any],\n) -> None:\n meta.base_path = controller_name or cls.__name__\n meta.tag = cast(str | None, options.get(\"tag\")) or controller_name or cls.__name__\n meta.auth = cast(str | None, options.get(\"auth\"))\n meta.rate_limit = options.get(\"rateLimit\", options.get(\"rate_limit\"))\n meta.token_limit = options.get(\"tokenLimit\", options.get(\"token_limit\"))\n meta.cache = options.get(\"cache\")\n\n\ndef _route_decorator(method: HttpMethod):\n def factory(\n arg: str | Mapping[str, Any] | F | None = None,\n opts: Mapping[str, Any] | None = None,\n ):\n if callable(arg):\n return _decorate_route(cast(F, arg), method, None, {})\n\n decorator_path: str | None = None\n options: dict[str, Any] = dict(opts or {})\n if isinstance(arg, str):\n decorator_path = arg\n elif isinstance(arg, Mapping):\n options = dict(arg)\n elif arg is not None:\n raise TypeError(f\"Unsupported decorator argument for {method}: {arg!r}\")\n\n def decorator(fn: F) -> F:\n return _decorate_route(fn, method, decorator_path, options)\n\n return decorator\n\n return factory\n\n\ndef _decorate_route(\n fn: F,\n method: HttpMethod,\n decorator_path: str | None,\n options: dict[str, Any],\n) -> F:\n meta = RouteMeta(\n name=fn.__name__,\n method=method,\n path=f\"/{fn.__name__}\",\n decorator_path=decorator_path,\n auth=cast(str | None, options.get(\"auth\")),\n summary=cast(str | None, options.get(\"summary\")),\n description=cast(str | None, options.get(\"description\")),\n rate_limit=options.get(\"rateLimit\", options.get(\"rate_limit\")),\n token_limit=options.get(\"tokenLimit\", options.get(\"token_limit\")),\n cache=options.get(\"cache\"),\n opts=options or None,\n )\n fn.__dict__[ROUTE_METADATA_KEY] = meta\n return fn\n\n\nGET = _route_decorator(\"GET\")\nPOST = _route_decorator(\"POST\")\nPUT = _route_decorator(\"PUT\")\nPATCH = _route_decorator(\"PATCH\")\nDELETE = _route_decorator(\"DELETE\")\n",
6
+ "plat_browser/errors.py": "from __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\n\n@dataclass\nclass HttpResponse(Exception):\n status_code: int\n body: Any = None\n headers: dict[str, str] = field(default_factory=dict)\n\n def __post_init__(self) -> None:\n super().__init__(f\"HTTP {self.status_code}\")\n\n\nclass HttpError(HttpResponse):\n def __init__(\n self,\n status_code: int,\n message: str,\n data: Any = None,\n headers: dict[str, str] | None = None,\n ) -> None:\n body: dict[str, Any] = {\"error\": message}\n if data is not None:\n body[\"data\"] = data\n super().__init__(status_code=status_code, body=body, headers=headers or {})\n self.message = message\n self.data = data\n",
7
+ "plat_browser/metadata.py": "from __future__ import annotations\n\nfrom .types import ControllerMeta\n\n\nCONTROLLER_METADATA_KEY = \"__plat_browser_controller_meta__\"\n\n\ndef get_controller_meta(ctor: type) -> ControllerMeta | None:\n return getattr(ctor, CONTROLLER_METADATA_KEY, None)\n\n\ndef ensure_controller_meta(ctor: type) -> ControllerMeta:\n meta = get_controller_meta(ctor)\n if meta is None:\n meta = ControllerMeta()\n setattr(ctor, CONTROLLER_METADATA_KEY, meta)\n return meta\n",
8
+ "plat_browser/plugins.py": "from __future__ import annotations\n\nimport json\nimport math\nimport time\nfrom dataclasses import dataclass, field\nfrom typing import Any, Callable, Protocol\n\nfrom .errors import HttpError\n\n\n@dataclass\nclass BucketConfig:\n max_balance: float\n fill_interval: int\n fill_amount: float\n refunded_status_codes: list[int] | None = None\n refund_successful: bool | None = None\n min_balance: float = 0\n\n\n@dataclass\nclass RateLimitEntry:\n key: str | None = None\n cost: float | None = None\n config: BucketConfig | None = None\n\n\nRateLimitMeta = RateLimitEntry | list[RateLimitEntry]\n\n\n@dataclass\nclass ResolvedRateLimitEntry:\n key: str\n cost: float\n config: BucketConfig\n\n\n@dataclass\nclass RateLimitContext:\n entries: list[ResolvedRateLimitEntry]\n remaining_balances: list[float]\n\n\nclass RateLimitController(Protocol):\n def check(self, key: str, config: BucketConfig) -> float:\n ...\n\n def deduct(self, key: str, cost: float, config: BucketConfig) -> float:\n ...\n\n def refund(self, key: str, cost: float, config: BucketConfig) -> None:\n ...\n\n\n@dataclass\nclass TokenCallCostFormula:\n initial: float | None = None\n per_limit: float | None = None\n per_char: float | None = None\n\n\n@dataclass\nclass TokenResponseCostFormula:\n per_ms: float | None = None\n per_item: float | None = None\n per_char: float | None = None\n per_key: float | None = None\n\n\nTokenCallCostSpec = float | int | TokenCallCostFormula | Callable[[Any, Any], float]\nTokenResponseCostSpec = float | int | TokenResponseCostFormula | Callable[[Any, Any, Any], float]\nTokenFailureCostSpec = float | int | Callable[[Exception, Any, Any], float]\n\n\n@dataclass\nclass TokenLimitEntry:\n key: str | None = None\n call_cost: TokenCallCostSpec | None = None\n response_cost: TokenResponseCostSpec | None = None\n failure_cost: TokenFailureCostSpec | None = None\n config: BucketConfig | None = None\n\n\nTokenLimitMeta = TokenLimitEntry | list[TokenLimitEntry]\n\n\n@dataclass\nclass TokenLimitTiming:\n start_ms: int\n end_ms: int\n duration_ms: int\n\n\n@dataclass\nclass ResolvedTokenLimitEntry:\n key: str\n call_cost: float\n response_cost_spec: TokenResponseCostSpec | None\n failure_cost_spec: TokenFailureCostSpec | None\n config: BucketConfig\n\n\n@dataclass\nclass TokenLimitContext:\n entries: list[ResolvedTokenLimitEntry]\n remaining_balances: list[float]\n response_costs: list[float] = field(default_factory=list)\n failure_costs: list[float] = field(default_factory=list)\n timing: TokenLimitTiming | None = None\n\n\nclass TokenLimitController(Protocol):\n def check(self, key: str, config: BucketConfig) -> float:\n ...\n\n def deduct(self, key: str, cost: float, config: BucketConfig) -> float:\n ...\n\n def refund(self, key: str, cost: float, config: BucketConfig) -> None:\n ...\n\n\n@dataclass\nclass CacheEntry:\n key: str\n ttl: int | None = None\n methods: list[str] | None = None\n\n\nCacheMeta = CacheEntry | list[CacheEntry]\n\n\n@dataclass\nclass CacheContext:\n key: str | None\n hit: bool\n stored: bool\n\n\nclass CacheController(Protocol):\n def get(self, key: str) -> Any:\n ...\n\n def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None:\n ...\n\n def clear(self, key: str) -> None:\n ...\n\n\nRateLimitConfigs = dict[str, BucketConfig]\nTokenLimitConfigs = dict[str, BucketConfig]\n\n\ndef resolve_rate_limit_key(raw: str, method_name: str, base_path: str, user: Any = None) -> str:\n return _resolve_common_key(raw, method_name, base_path, user)\n\n\ndef resolve_token_limit_key(raw: str, method_name: str, base_path: str, user: Any = None) -> str:\n return _resolve_common_key(raw, method_name, base_path, user)\n\n\ndef resolve_cache_key(template: str, params: dict[str, Any], method_name: str, base_path: str, user: Any = None) -> str:\n key = _resolve_common_key(template, method_name, base_path, user)\n for match in set(part for part in _find_template_matches(key, \"{\", \"}\")):\n name = match[1:-1]\n value = params.get(name)\n if value is not None:\n key = key.replace(match, str(value))\n return key\n\n\ndef create_in_memory_rate_limit() -> RateLimitController:\n buckets: dict[str, dict[str, float]] = {}\n return _create_in_memory_bucket_controller(buckets, \"Rate limit exceeded\")\n\n\ndef create_in_memory_token_limit() -> TokenLimitController:\n buckets: dict[str, dict[str, float]] = {}\n return _create_in_memory_bucket_controller(buckets, \"Token limit exceeded\")\n\n\ndef create_in_memory_cache() -> CacheController:\n store: dict[str, dict[str, Any]] = {}\n\n class Controller:\n def get(self, key: str) -> Any:\n entry = store.get(key)\n if entry is None:\n return None\n expires_at = entry.get(\"expires_at\")\n if expires_at is not None and time.time() * 1000 > expires_at:\n store.pop(key, None)\n return None\n return entry[\"value\"]\n\n def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None:\n expires_at = None\n if ttl_seconds is not None:\n expires_at = time.time() * 1000 + ttl_seconds * 1000\n store[key] = {\"value\": value, \"expires_at\": expires_at}\n\n def clear(self, key: str) -> None:\n store.pop(key, None)\n\n return Controller()\n\n\ndef apply_rate_limit_check(\n meta: RateLimitMeta | None,\n controller: RateLimitController,\n configs: RateLimitConfigs,\n method_name: str,\n base_path: str,\n user: Any = None,\n) -> tuple[list[ResolvedRateLimitEntry], list[float]]:\n if meta is None:\n return [], []\n\n entries = meta if isinstance(meta, list) else [meta]\n resolved: list[ResolvedRateLimitEntry] = []\n remaining_balances: list[float] = []\n for entry in entries:\n raw_key = entry.key or \":route\"\n key = resolve_rate_limit_key(raw_key, method_name, base_path, user)\n cost = float(entry.cost if entry.cost is not None else 1)\n config = entry.config or configs.get(key)\n if config is None:\n raise ValueError(f'Rate limit config not found for key \"{key}\".')\n remaining = controller.deduct(key, cost, config)\n resolved.append(ResolvedRateLimitEntry(key=key, cost=cost, config=config))\n remaining_balances.append(remaining)\n return resolved, remaining_balances\n\n\ndef apply_rate_limit_refund(\n entries: list[ResolvedRateLimitEntry],\n controller: RateLimitController,\n status_code: int,\n) -> None:\n for entry in entries:\n config = entry.config\n should_refund = (\n (config.refunded_status_codes and status_code in config.refunded_status_codes)\n or (config.refund_successful and 200 <= status_code < 300)\n )\n if should_refund:\n controller.refund(entry.key, entry.cost, config)\n\n\ndef apply_token_limit_check(\n meta: TokenLimitMeta | None,\n controller: TokenLimitController,\n configs: TokenLimitConfigs,\n method_name: str,\n base_path: str,\n params: Any,\n ctx: Any,\n user: Any = None,\n) -> tuple[list[ResolvedTokenLimitEntry], list[float]]:\n if meta is None:\n return [], []\n\n entries = meta if isinstance(meta, list) else [meta]\n resolved: list[ResolvedTokenLimitEntry] = []\n remaining_balances: list[float] = []\n for entry in entries:\n raw_key = entry.key or \":route\"\n key = resolve_token_limit_key(raw_key, method_name, base_path, user)\n call_cost = resolve_call_cost(entry.call_cost if entry.call_cost is not None else 1, params, ctx)\n config = entry.config or configs.get(key)\n if config is None:\n raise ValueError(f'Token limit config not found for key \"{key}\".')\n remaining = controller.deduct(key, call_cost, config)\n resolved.append(\n ResolvedTokenLimitEntry(\n key=key,\n call_cost=call_cost,\n response_cost_spec=entry.response_cost,\n failure_cost_spec=entry.failure_cost,\n config=config,\n )\n )\n remaining_balances.append(remaining)\n return resolved, remaining_balances\n\n\ndef apply_token_limit_response(\n entries: list[ResolvedTokenLimitEntry],\n controller: TokenLimitController,\n result: Any,\n timing: TokenLimitTiming,\n params: Any,\n status_code: int,\n) -> list[float]:\n response_costs: list[float] = []\n for entry in entries:\n config = entry.config\n should_refund_call = (\n (config.refunded_status_codes and status_code in config.refunded_status_codes)\n or (config.refund_successful and 200 <= status_code < 300)\n )\n if should_refund_call:\n controller.refund(entry.key, entry.call_cost, config)\n continue\n response_cost = resolve_response_cost(entry.response_cost_spec, result, timing, params)\n if response_cost > 0:\n controller.deduct(entry.key, response_cost, config)\n response_costs.append(response_cost)\n return response_costs\n\n\ndef apply_token_limit_failure(\n entries: list[ResolvedTokenLimitEntry],\n controller: TokenLimitController,\n error: Exception,\n timing: TokenLimitTiming,\n params: Any,\n status_code: int,\n) -> list[float]:\n failure_costs: list[float] = []\n for entry in entries:\n failure_cost = resolve_failure_cost(entry.failure_cost_spec, error, timing, params)\n if failure_cost > 0:\n controller.deduct(entry.key, failure_cost, entry.config)\n elif failure_cost < 0:\n controller.refund(entry.key, -failure_cost, entry.config)\n else:\n config = entry.config\n should_refund_call = (\n (config.refunded_status_codes and status_code in config.refunded_status_codes)\n or (config.refund_successful and 200 <= status_code < 300)\n )\n if should_refund_call:\n controller.refund(entry.key, entry.call_cost, config)\n failure_costs.append(failure_cost)\n return failure_costs\n\n\ndef apply_cache_check(\n meta: CacheMeta | None,\n controller: CacheController,\n params: dict[str, Any],\n http_method: str,\n method_name: str,\n base_path: str,\n user: Any = None,\n) -> tuple[str | None, bool, Any, CacheEntry | None]:\n if meta is None:\n return None, False, None, None\n\n entries = meta if isinstance(meta, list) else [meta]\n for entry in entries:\n methods = entry.methods or [\"GET\"]\n if http_method not in methods:\n continue\n cache_key = resolve_cache_key(entry.key, params, method_name, base_path, user)\n cached_value = controller.get(cache_key)\n if cached_value is not None:\n return cache_key, True, cached_value, entry\n return cache_key, False, None, entry\n return None, False, None, None\n\n\ndef apply_cache_store(cache_key: str | None, entry: CacheEntry | None, controller: CacheController, result: Any) -> None:\n if cache_key is not None and entry is not None:\n controller.set(cache_key, result, entry.ttl)\n\n\ndef resolve_call_cost(spec: TokenCallCostSpec | None, params: Any, ctx: Any) -> float:\n if spec is None:\n return 1\n if isinstance(spec, (int, float)):\n return float(spec)\n if isinstance(spec, TokenCallCostFormula):\n limit = _get_value(params, \"limit\", 0)\n param_chars = len(json.dumps(params, default=str))\n return (\n float(spec.initial or 0)\n + float(spec.per_limit or 0) * float(limit)\n + float(spec.per_char or 0) * param_chars\n )\n return float(spec(params, ctx))\n\n\ndef resolve_response_cost(spec: TokenResponseCostSpec | None, result: Any, timing: TokenLimitTiming, params: Any) -> float:\n if spec in {None, 0}:\n return 0\n if isinstance(spec, (int, float)):\n return float(spec)\n if isinstance(spec, TokenResponseCostFormula):\n return (\n float(spec.per_ms or 0) * timing.duration_ms\n + float(spec.per_item or 0) * _count_items(result)\n + float(spec.per_char or 0) * len(json.dumps(result, default=str))\n + float(spec.per_key or 0) * _count_keys(result)\n )\n return float(spec(result, timing, params))\n\n\ndef resolve_failure_cost(spec: TokenFailureCostSpec | None, error: Exception, timing: TokenLimitTiming, params: Any) -> float:\n if spec in {None, 0}:\n return 0\n if isinstance(spec, (int, float)):\n return float(spec)\n return float(spec(error, timing, params))\n\n\ndef _resolve_common_key(raw: str, method_name: str, base_path: str, user: Any = None) -> str:\n key = raw.replace(\":route\", method_name).replace(\":parent\", base_path)\n if user is not None:\n user_id = _get_value(user, \"sub\") or _get_value(user, \"id\")\n if user_id is not None:\n key = key.replace(\":user\", str(user_id))\n tier = _get_value(user, \"tier\") or _get_value(user, \"plan\")\n if tier is not None:\n key = key.replace(\":tier\", str(tier))\n for match in set(part for part in _find_user_tokens(key)):\n field = match[6:]\n value = _get_value(user, field)\n if value is not None:\n key = key.replace(match, str(value))\n return key\n\n\ndef _find_user_tokens(value: str) -> list[str]:\n parts: list[str] = []\n start = 0\n token = \":user:\"\n while True:\n idx = value.find(token, start)\n if idx < 0:\n break\n end = idx + len(token)\n while end < len(value) and (value[end].isalnum() or value[end] == \"_\"):\n end += 1\n parts.append(value[idx:end])\n start = end\n return parts\n\n\ndef _find_template_matches(value: str, open_char: str, close_char: str) -> list[str]:\n parts: list[str] = []\n start = 0\n while True:\n left = value.find(open_char, start)\n if left < 0:\n break\n right = value.find(close_char, left + 1)\n if right < 0:\n break\n parts.append(value[left : right + 1])\n start = right + 1\n return parts\n\n\ndef _get_value(obj: Any, key: str, default: Any = None) -> Any:\n if isinstance(obj, dict):\n return obj.get(key, default)\n return getattr(obj, key, default)\n\n\ndef _count_items(value: Any) -> int:\n if isinstance(value, list):\n return len(value)\n if isinstance(value, dict):\n return 1\n return 0\n\n\ndef _count_keys(value: Any) -> int:\n if isinstance(value, dict):\n return len(value.keys())\n return 0\n\n\ndef _create_in_memory_bucket_controller(\n buckets: dict[str, dict[str, float]],\n message: str,\n):\n class Controller:\n def check(self, key: str, config: BucketConfig) -> float:\n return _refill_bucket(buckets, key, config)\n\n def deduct(self, key: str, cost: float, config: BucketConfig) -> float:\n balance = _refill_bucket(buckets, key, config)\n new_balance = balance - cost\n min_balance = config.min_balance\n if new_balance < min_balance:\n deficit = min_balance - new_balance\n intervals_needed = math.ceil(deficit / config.fill_amount)\n retry_after_ms = intervals_needed * config.fill_interval\n raise HttpError(429, message, {\"retry_after_ms\": retry_after_ms})\n buckets[key][\"balance\"] = new_balance\n return new_balance\n\n def refund(self, key: str, cost: float, config: BucketConfig) -> None:\n if key not in buckets:\n buckets[key] = {\n \"balance\": min(config.max_balance, cost),\n \"last_refill_ms\": time.time() * 1000,\n }\n else:\n buckets[key][\"balance\"] = min(config.max_balance, buckets[key][\"balance\"] + cost)\n\n return Controller()\n\n\ndef _refill_bucket(buckets: dict[str, dict[str, float]], key: str, config: BucketConfig) -> float:\n now = time.time() * 1000\n bucket = buckets.get(key)\n if bucket is None:\n buckets[key] = {\"balance\": config.max_balance, \"last_refill_ms\": now}\n return config.max_balance\n\n elapsed_ms = now - bucket[\"last_refill_ms\"]\n intervals_elapsed = math.floor(elapsed_ms / config.fill_interval)\n if intervals_elapsed > 0:\n generated = intervals_elapsed * config.fill_amount\n bucket[\"balance\"] = min(config.max_balance, bucket[\"balance\"] + generated)\n bucket[\"last_refill_ms\"] = now\n return bucket[\"balance\"]\n",
9
+ "plat_browser/server.py": "from __future__ import annotations\n\nimport ast\nimport inspect\nimport logging\nfrom dataclasses import dataclass, field\nimport time\nimport sys\nfrom typing import Any, Awaitable, Callable, Literal, Mapping, get_args, get_origin, get_type_hints\n\nfrom .decorators import ROUTE_METADATA_KEY\nfrom .errors import HttpError\nfrom .metadata import get_controller_meta\nfrom .plugins import (\n BucketConfig,\n CacheEntry,\n CacheMeta,\n CacheContext,\n RateLimitContext,\n RateLimitEntry,\n RateLimitMeta,\n TokenLimitContext,\n TokenLimitEntry,\n TokenLimitMeta,\n TokenLimitTiming,\n apply_cache_check,\n apply_cache_store,\n apply_rate_limit_check,\n apply_rate_limit_refund,\n apply_token_limit_check,\n apply_token_limit_failure,\n apply_token_limit_response,\n create_in_memory_cache,\n create_in_memory_rate_limit,\n create_in_memory_token_limit,\n)\nfrom .types import ControllerMeta, RouteContext, RouteMeta\n\n\nUndecoratedMode = Literal[\"GET\", \"POST\", \"private\"]\nServerMessage = Mapping[str, Any]\nRESERVED_METHOD_NAMES = {\"tools\", \"routes\", \"endpoints\", \"help\", \"openapi\"}\nLEGACY_PAYLOAD_PARAM_NAMES = {\"input\", \"payload\", \"body\", \"data\"}\nPYTHON_BROWSER_STDLIB_MODULES = {\n \"__future__\",\n \"abc\",\n \"argparse\",\n \"asyncio\",\n \"base64\",\n \"collections\",\n \"copy\",\n \"csv\",\n \"dataclasses\",\n \"datetime\",\n \"decimal\",\n \"enum\",\n \"functools\",\n \"hashlib\",\n \"itertools\",\n \"json\",\n \"logging\",\n \"math\",\n \"pathlib\",\n \"random\",\n \"re\",\n \"statistics\",\n \"string\",\n \"sys\",\n \"textwrap\",\n \"time\",\n \"typing\",\n \"uuid\",\n}\n\n\n@dataclass(slots=True)\nclass BrowserPackagePlan:\n python_source: str\n requested_packages: list[str] = field(default_factory=list)\n imported_modules: list[str] = field(default_factory=list)\n\n\n@dataclass(slots=True)\nclass BrowserServerDefinition:\n server_name: str\n controllers: list[type[Any]]\n requested_packages: list[str] = field(default_factory=list)\n imported_modules: list[str] = field(default_factory=list)\n options: dict[str, Any] = field(default_factory=dict)\n\n\n@dataclass(slots=True)\nclass BrowserResolvedOperation:\n method_name: str\n method: str\n path: str\n bound_method: Any\n route_meta: RouteMeta\n controller_meta: ControllerMeta\n input_annotation: Any\n output_annotation: Any\n params: list[inspect.Parameter]\n\n\nBrowserMiddlewareContext = dict[str, Any]\nBrowserMiddleware = Callable[[BrowserMiddlewareContext, Callable[[], Awaitable[Any]]], Awaitable[Any] | Any]\n\n\nclass CallBridge:\n def __init__(self, emit=None):\n self._emit = emit\n\n def emit(self, event: str, data: Any) -> None:\n if callable(self._emit):\n self._emit({\"event\": event, \"data\": data})\n\n def progress(self, data: Any) -> None:\n self.emit(\"progress\", data)\n\n def log(self, data: Any) -> None:\n self.emit(\"log\", data)\n\n def chunk(self, data: Any) -> None:\n self.emit(\"chunk\", data)\n\n def message(self, data: Any) -> None:\n self.emit(\"message\", data)\n\n\nclass BrowserPLATServer:\n def __init__(\n self,\n options: Mapping[str, Any] | None = None,\n *controller_classes: type[Any],\n server_name: str | None = None,\n undecorated_mode: UndecoratedMode = \"POST\",\n ) -> None:\n self.options = dict(options or {})\n self.server_name = server_name or self.options.get(\"server_name\") or \"browser-python-server\"\n self.undecorated_mode = cast_undecorated_mode(self.options.get(\"undecorated_mode\"), undecorated_mode)\n self.logger = self.options.get(\"logger\") or logging.getLogger(\"plat_browser.server\")\n self.middleware: list[BrowserMiddleware] = list(self.options.get(\"middleware\") or [])\n self.routes: list[dict[str, str]] = []\n self.tools: dict[str, dict[str, Any]] = {}\n self.operations_by_id: dict[str, BrowserResolvedOperation] = {}\n self.operations_by_route: dict[str, BrowserResolvedOperation] = {}\n self.registered_method_names: set[str] = set()\n self.registered_controller_names: set[str] = set()\n self._setup_policy_defaults()\n if controller_classes:\n self.register(*controller_classes)\n\n def use(self, middleware: BrowserMiddleware) -> \"BrowserPLATServer\":\n self.middleware.append(middleware)\n return self\n\n @property\n def openapi(self) -> dict[str, Any]:\n return self.generate_openapi()\n\n def _setup_policy_defaults(self) -> None:\n if self.options.get(\"rate_limit\") is not None and not self.options[\"rate_limit\"].get(\"controller\"):\n self.options[\"rate_limit\"][\"controller\"] = create_in_memory_rate_limit()\n if self.options.get(\"token_limit\") is not None and not self.options[\"token_limit\"].get(\"controller\"):\n self.options[\"token_limit\"][\"controller\"] = create_in_memory_token_limit()\n if self.options.get(\"cache\") is not None and not self.options[\"cache\"].get(\"controller\"):\n self.options[\"cache\"][\"controller\"] = create_in_memory_cache()\n\n def register(self, *controller_classes: type[Any]) -> \"BrowserPLATServer\":\n for controller_class in controller_classes:\n meta = get_controller_meta(controller_class) or ControllerMeta(\n base_path=controller_class.__name__,\n tag=controller_class.__name__,\n )\n instance = controller_class()\n controller_tag = meta.tag or meta.base_path or controller_class.__name__\n lower_controller_tag = controller_tag.lower()\n if lower_controller_tag in RESERVED_METHOD_NAMES:\n raise ValueError(f\"Controller '{controller_tag}' uses a reserved plat system name.\")\n if controller_tag in self.registered_method_names:\n raise ValueError(f\"Controller '{controller_tag}' conflicts with an existing method name.\")\n self.registered_controller_names.add(controller_tag)\n\n methods = inspect.getmembers(controller_class, predicate=inspect.isfunction)\n for method_name, method_fn in methods:\n route_meta = getattr(method_fn, ROUTE_METADATA_KEY, None)\n if route_meta is None:\n route_meta = self._build_implicit_route_meta(method_name)\n if route_meta is None:\n continue\n self._hydrate_route_meta(route_meta, method_fn)\n self._validate_method_name(method_name, controller_class.__name__)\n if method_name in self.registered_controller_names:\n raise ValueError(f\"Method '{method_name}' in {controller_class.__name__} conflicts with controller name '{method_name}'.\")\n if method_name in self.registered_method_names:\n raise ValueError(f'Duplicate operationId: method \"{method_name}\" is defined in multiple controllers.')\n self.registered_method_names.add(method_name)\n\n bound_method = getattr(instance, method_name)\n full_path = route_meta.path or f\"/{method_name}\"\n operation = BrowserResolvedOperation(\n method_name=method_name,\n method=(route_meta.method or \"POST\").upper(),\n path=full_path,\n bound_method=bound_method,\n route_meta=route_meta,\n controller_meta=meta,\n input_annotation=route_meta.input_model,\n output_annotation=route_meta.output_model,\n params=list(inspect.signature(method_fn).parameters.values())[1:],\n )\n self.operations_by_id[method_name] = operation\n self.operations_by_route[f\"{operation.method} {operation.path}\"] = operation\n self.routes.append({\"method\": operation.method, \"path\": operation.path, \"methodName\": method_name})\n self.tools[method_name] = {\n \"name\": method_name,\n \"summary\": route_meta.summary,\n \"description\": route_meta.description or f\"{operation.method} {full_path}\",\n \"method\": operation.method,\n \"path\": full_path,\n \"controller\": controller_tag,\n \"tags\": [controller_tag],\n \"input_schema\": self._build_schema(route_meta.input_model),\n \"response_schema\": self._build_schema(route_meta.output_model),\n }\n return self\n\n def generate_openapi(self) -> dict[str, Any]:\n paths: dict[str, dict[str, Any]] = {}\n for operation in self.operations_by_id.values():\n path_item = paths.setdefault(operation.path, {})\n entry: dict[str, Any] = {\n \"operationId\": operation.method_name,\n \"summary\": operation.route_meta.summary,\n \"description\": operation.route_meta.description,\n \"responses\": {\"200\": {\"description\": \"ok\"}},\n }\n input_schema = self._build_schema(operation.input_annotation)\n if input_schema is not None:\n entry[\"requestBody\"] = {\n \"required\": True,\n \"content\": {\"application/json\": {\"schema\": input_schema}},\n }\n response_schema = self._build_schema(operation.output_annotation)\n if response_schema is not None:\n entry[\"responses\"][\"200\"][\"content\"] = {\"application/json\": {\"schema\": response_schema}}\n path_item[operation.method.lower()] = trim_none(entry)\n return {\n \"openapi\": \"3.1.0\",\n \"info\": {\"title\": \"plat browser python server\", \"version\": \"0.1.0\"},\n \"paths\": paths,\n }\n\n async def handle_request(self, message: ServerMessage, emit=None) -> Any:\n message = normalize_server_message(message)\n operation = self._resolve_operation(\n operation_id=coerce_str(message.get(\"operationId\")),\n method=coerce_str(message.get(\"method\")) or \"POST\",\n path=coerce_str(message.get(\"path\")) or \"\",\n )\n if operation is None:\n raise KeyError(\"No browser operation matched the incoming request.\")\n\n payload = (\n message.get(\"input\")\n if \"input\" in message\n else message.get(\"body\")\n if \"body\" in message\n else message.get(\"params\")\n if \"params\" in message\n else {}\n )\n ctx = RouteContext(\n method=operation.method,\n url=operation.path,\n headers=dict(message.get(\"headers\") or {}),\n call=CallBridge(emit),\n opts={\"transport\": \"browser-python\"},\n request=dict(message),\n )\n status_code = 200\n rate_limit_entries = []\n token_limit_entries = []\n token_limit_start_ms = int(time.time() * 1000)\n handler_called = False\n on_request = self.options.get(\"on_request\") or self.options.get(\"onRequest\")\n on_response = self.options.get(\"on_response\") or self.options.get(\"onResponse\")\n on_error = self.options.get(\"on_error\") or self.options.get(\"onError\")\n if callable(on_request):\n maybe = on_request(message, ctx)\n if inspect.isawaitable(maybe):\n await maybe\n auth_mode = operation.route_meta.auth or operation.controller_meta.auth or self.options.get(\"default_auth\")\n auth_handler = self.options.get(\"auth\")\n if auth_mode and auth_handler and callable(auth_handler.get(\"verify\")):\n auth_result = auth_handler[\"verify\"](auth_mode, message, ctx)\n if inspect.isawaitable(auth_result):\n auth_result = await auth_result\n ctx.auth = auth_result\n rate_limit_meta = self._get_rate_limit_meta(operation.route_meta, operation.controller_meta)\n if rate_limit_meta is not None and self.options.get(\"rate_limit\", {}).get(\"controller\"):\n entries, balances = apply_rate_limit_check(\n rate_limit_meta,\n self.options[\"rate_limit\"][\"controller\"],\n self.options[\"rate_limit\"].get(\"configs\", {}),\n operation.method_name,\n operation.controller_meta.base_path,\n ctx.auth,\n )\n rate_limit_entries = entries\n ctx.rate_limit = RateLimitContext(entries=entries, remaining_balances=balances)\n token_limit_meta = self._get_token_limit_meta(operation.route_meta, operation.controller_meta)\n if token_limit_meta is not None and self.options.get(\"token_limit\", {}).get(\"controller\"):\n entries, balances = apply_token_limit_check(\n token_limit_meta,\n self.options[\"token_limit\"][\"controller\"],\n self.options[\"token_limit\"].get(\"configs\", {}),\n operation.method_name,\n operation.controller_meta.base_path,\n payload,\n ctx,\n ctx.auth,\n )\n token_limit_entries = entries\n ctx.token_limit = TokenLimitContext(entries=entries, remaining_balances=balances)\n cache_key = None\n cache_entry = None\n cache_meta = self._get_cache_meta(operation.route_meta, operation.controller_meta)\n if cache_meta is not None and self.options.get(\"cache\", {}).get(\"controller\"):\n cache_key, hit, cached_value, cache_entry = apply_cache_check(\n cache_meta,\n self.options[\"cache\"][\"controller\"],\n dict(payload) if isinstance(payload, Mapping) else {},\n operation.method,\n operation.method_name,\n operation.controller_meta.base_path,\n ctx.auth,\n )\n ctx.cache = CacheContext(key=cache_key, hit=hit, stored=False)\n if hit:\n return cached_value\n middleware_context: BrowserMiddlewareContext = {\n \"request\": dict(message),\n \"operation\": operation,\n \"ctx\": ctx,\n \"input\": payload,\n \"logger\": self.logger,\n }\n try:\n handler_called = True\n result = await self._run_middleware_chain(\n middleware_context,\n lambda: self._invoke_handler(operation.bound_method, operation.params, payload, ctx),\n )\n if token_limit_entries and self.options.get(\"token_limit\", {}).get(\"controller\"):\n timing = TokenLimitTiming(\n start_ms=token_limit_start_ms,\n end_ms=int(time.time() * 1000),\n duration_ms=max(0, int(time.time() * 1000) - token_limit_start_ms),\n )\n response_costs = apply_token_limit_response(\n token_limit_entries,\n self.options[\"token_limit\"][\"controller\"],\n result,\n timing,\n payload,\n status_code,\n )\n if ctx.token_limit:\n ctx.token_limit.timing = timing\n ctx.token_limit.response_costs = response_costs\n if rate_limit_entries and self.options.get(\"rate_limit\", {}).get(\"controller\"):\n apply_rate_limit_refund(rate_limit_entries, self.options[\"rate_limit\"][\"controller\"], status_code)\n if cache_entry is not None and self.options.get(\"cache\", {}).get(\"controller\"):\n apply_cache_store(cache_key, cache_entry, self.options[\"cache\"][\"controller\"], result)\n if ctx.cache:\n ctx.cache.stored = True\n if callable(on_response):\n maybe = on_response(message, ctx, result)\n if inspect.isawaitable(maybe):\n await maybe\n return result\n except Exception as error:\n status_code = getattr(error, \"status_code\", 500)\n if token_limit_entries and self.options.get(\"token_limit\", {}).get(\"controller\") and handler_called:\n timing = TokenLimitTiming(\n start_ms=token_limit_start_ms,\n end_ms=int(time.time() * 1000),\n duration_ms=max(0, int(time.time() * 1000) - token_limit_start_ms),\n )\n failure_costs = apply_token_limit_failure(\n token_limit_entries,\n self.options[\"token_limit\"][\"controller\"],\n error,\n timing,\n payload,\n status_code,\n )\n if ctx.token_limit:\n ctx.token_limit.timing = timing\n ctx.token_limit.failure_costs = failure_costs\n if rate_limit_entries and self.options.get(\"rate_limit\", {}).get(\"controller\"):\n apply_rate_limit_refund(rate_limit_entries, self.options[\"rate_limit\"][\"controller\"], status_code)\n self.logger.error(\"BrowserPLATServer request failed\", exc_info=error)\n if callable(on_error):\n maybe = on_error(message, ctx, error)\n if inspect.isawaitable(maybe):\n await maybe\n raise\n\n def _resolve_operation(self, *, operation_id: str | None, method: str, path: str) -> BrowserResolvedOperation | None:\n if operation_id and operation_id in self.operations_by_id:\n return self.operations_by_id[operation_id]\n return self.operations_by_route.get(f\"{method.upper()} {path}\")\n\n async def _run_middleware_chain(\n self,\n context: BrowserMiddlewareContext,\n final_handler: Callable[[], Any],\n ) -> Any:\n index = -1\n\n async def dispatch(next_index: int) -> Any:\n nonlocal index\n if next_index <= index:\n raise RuntimeError(\"Browser middleware called next() multiple times\")\n index = next_index\n if next_index >= len(self.middleware):\n result = final_handler()\n if inspect.isawaitable(result):\n return await result\n return result\n middleware = self.middleware[next_index]\n result = middleware(context, lambda: dispatch(next_index + 1))\n if inspect.isawaitable(result):\n return await result\n return result\n\n return await dispatch(0)\n\n def _invoke_handler(self, bound_method: Any, params: list[inspect.Parameter], parsed_input: Any, ctx: RouteContext) -> Any:\n if not params:\n return bound_method()\n non_context_params = [param for param in params if not self._is_context_parameter(param)]\n use_legacy_payload_param = (\n len(non_context_params) == 1\n and non_context_params[0].name in LEGACY_PAYLOAD_PARAM_NAMES\n )\n is_mapping_payload = isinstance(parsed_input, Mapping)\n\n if len(params) == 1:\n only_param = params[0]\n if self._is_context_parameter(only_param):\n return bound_method(ctx)\n if use_legacy_payload_param or not is_mapping_payload:\n return bound_method(parsed_input)\n if only_param.kind is inspect.Parameter.VAR_KEYWORD:\n return bound_method(**dict(parsed_input))\n return bound_method(**{only_param.name: parsed_input.get(only_param.name, parsed_input)})\n\n args: list[Any] = []\n kwargs: dict[str, Any] = {}\n payload_mapping = dict(parsed_input) if is_mapping_payload else {}\n\n for param in params:\n if self._is_context_parameter(param):\n if param.kind is inspect.Parameter.POSITIONAL_ONLY:\n args.append(ctx)\n else:\n kwargs[param.name] = ctx\n continue\n\n if use_legacy_payload_param:\n if param.kind is inspect.Parameter.POSITIONAL_ONLY:\n args.append(parsed_input)\n else:\n kwargs[param.name] = parsed_input\n continue\n\n if param.kind is inspect.Parameter.VAR_KEYWORD:\n kwargs.update(payload_mapping)\n continue\n\n if not is_mapping_payload:\n if param.kind is inspect.Parameter.POSITIONAL_ONLY:\n args.append(parsed_input)\n else:\n kwargs[param.name] = parsed_input\n continue\n\n if param.name not in payload_mapping:\n continue\n\n if param.kind is inspect.Parameter.POSITIONAL_ONLY:\n args.append(payload_mapping[param.name])\n else:\n kwargs[param.name] = payload_mapping[param.name]\n\n return bound_method(*args, **kwargs)\n\n def _is_context_parameter(self, param: inspect.Parameter) -> bool:\n return param.annotation is RouteContext or param.name in {\"ctx\", \"context\"}\n\n def _build_implicit_route_meta(self, method_name: str) -> RouteMeta | None:\n if self.undecorated_mode == \"private\" or method_name.startswith(\"_\"):\n return None\n return RouteMeta(name=method_name, method=self.undecorated_mode, path=f\"/{method_name}\")\n\n def _hydrate_route_meta(self, route_meta: RouteMeta, method_fn: Any) -> None:\n signature = inspect.signature(method_fn)\n params = list(signature.parameters.values())[1:]\n module = sys.modules.get(getattr(method_fn, \"__module__\", \"\"))\n try:\n resolved_hints = get_type_hints(\n method_fn,\n globalns=vars(module) if module is not None else None,\n )\n except Exception:\n resolved_hints = {}\n input_params = [\n param.replace(annotation=resolved_hints.get(param.name, param.annotation))\n for param in params\n if not self._is_context_parameter(param)\n ]\n route_meta.input_model = build_input_model_from_params(input_params)\n route_meta.output_model = resolved_hints.get(\"return\", signature.return_annotation)\n if route_meta.path is None:\n route_meta.path = f\"/{route_meta.name}\"\n if route_meta.method is None:\n route_meta.method = self.undecorated_mode if self.undecorated_mode != \"private\" else \"POST\"\n docstring = inspect.getdoc(method_fn)\n if docstring and not route_meta.summary:\n first_line, *_ = docstring.splitlines()\n route_meta.summary = first_line.strip() or None\n if len(docstring.splitlines()) > 1 and not route_meta.description:\n route_meta.description = docstring.strip()\n\n def _validate_method_name(self, method_name: str, controller_name: str) -> None:\n if not method_name:\n return\n if method_name[0].isalpha() and method_name[0].isupper():\n raise ValueError(f\"Method '{method_name}' in {controller_name} violates plat naming convention: method names must start with a lowercase letter.\")\n if \"_\" in method_name:\n raise ValueError(f\"Method '{method_name}' in {controller_name} violates plat naming convention: underscores are not allowed.\")\n if method_name.lower() in RESERVED_METHOD_NAMES:\n raise ValueError(f\"Method '{method_name}' in {controller_name} uses a reserved plat system name.\")\n\n def _build_schema(self, annotation: Any) -> dict[str, Any] | None:\n if isinstance(annotation, dict) and \"type\" in annotation:\n return annotation\n if annotation in {None, inspect.Signature.empty, Any}:\n return None\n return annotation_to_schema(annotation)\n\n def _get_rate_limit_meta(self, route_meta: RouteMeta, controller_meta: ControllerMeta):\n route_value = (route_meta.opts or {}).get(\"rateLimit\", route_meta.rate_limit)\n controller_value = getattr(controller_meta, \"rate_limit\", None)\n return self._coerce_rate_limit_meta(route_value or controller_value)\n\n def _get_token_limit_meta(self, route_meta: RouteMeta, controller_meta: ControllerMeta):\n route_value = (route_meta.opts or {}).get(\"tokenLimit\", route_meta.token_limit)\n controller_value = getattr(controller_meta, \"token_limit\", None)\n return self._coerce_token_limit_meta(route_value or controller_value)\n\n def _get_cache_meta(self, route_meta: RouteMeta, controller_meta: ControllerMeta):\n route_value = (route_meta.opts or {}).get(\"cache\", route_meta.cache)\n controller_value = getattr(controller_meta, \"cache\", None)\n return self._coerce_cache_meta(route_value or controller_value)\n\n def _coerce_rate_limit_meta(self, value: Any):\n if value is None:\n return None\n if isinstance(value, list):\n return [self._coerce_rate_limit_entry(item) for item in value]\n return self._coerce_rate_limit_entry(value)\n\n def _coerce_token_limit_meta(self, value: Any):\n if value is None:\n return None\n if isinstance(value, list):\n return [self._coerce_token_limit_entry(item) for item in value]\n return self._coerce_token_limit_entry(value)\n\n def _coerce_cache_meta(self, value: Any):\n if value is None:\n return None\n if isinstance(value, list):\n return [self._coerce_cache_entry(item) for item in value]\n return self._coerce_cache_entry(value)\n\n def _coerce_rate_limit_entry(self, value: Any) -> RateLimitEntry:\n if isinstance(value, RateLimitEntry):\n return value\n data = dict(value)\n if isinstance(data.get(\"config\"), dict):\n data[\"config\"] = self._coerce_bucket_config(data[\"config\"])\n return RateLimitEntry(\n key=data.get(\"key\"),\n cost=data.get(\"cost\"),\n config=data.get(\"config\"),\n )\n\n def _coerce_token_limit_entry(self, value: Any) -> TokenLimitEntry:\n if isinstance(value, TokenLimitEntry):\n return value\n data = dict(value)\n if isinstance(data.get(\"config\"), dict):\n data[\"config\"] = self._coerce_bucket_config(data[\"config\"])\n return TokenLimitEntry(\n key=data.get(\"key\"),\n call_cost=data.get(\"callCost\", data.get(\"call_cost\")),\n response_cost=data.get(\"responseCost\", data.get(\"response_cost\")),\n failure_cost=data.get(\"failureCost\", data.get(\"failure_cost\")),\n config=data.get(\"config\"),\n )\n\n def _coerce_cache_entry(self, value: Any) -> CacheEntry:\n if isinstance(value, CacheEntry):\n return value\n data = dict(value)\n return CacheEntry(\n key=data[\"key\"],\n ttl=data.get(\"ttl\"),\n methods=data.get(\"methods\"),\n )\n\n def _coerce_bucket_config(self, value: Any) -> BucketConfig:\n if isinstance(value, BucketConfig):\n return value\n data = dict(value)\n return BucketConfig(\n max_balance=data[\"maxBalance\"] if \"maxBalance\" in data else data[\"max_balance\"],\n fill_interval=data[\"fillInterval\"] if \"fillInterval\" in data else data[\"fill_interval\"],\n fill_amount=data[\"fillAmount\"] if \"fillAmount\" in data else data[\"fill_amount\"],\n refunded_status_codes=data.get(\"refundedStatusCodes\", data.get(\"refunded_status_codes\")),\n refund_successful=data.get(\"refundSuccessful\", data.get(\"refund_successful\")),\n min_balance=data.get(\"minBalance\", data.get(\"min_balance\", 0)),\n )\n\n\ndef create_browser_server(\n options: Mapping[str, Any] | None = None,\n *controller_classes: type[Any],\n server_name: str | None = None,\n undecorated_mode: UndecoratedMode = \"POST\",\n) -> BrowserPLATServer:\n return BrowserPLATServer(options, *controller_classes, server_name=server_name, undecorated_mode=undecorated_mode)\n\n\ndef serve_client_side_server(\n server_name: str,\n controllers: list[type[Any]] | tuple[type[Any], ...],\n **options: Any,\n) -> BrowserServerDefinition:\n definition = BrowserServerDefinition(\n server_name=server_name,\n controllers=list(controllers),\n requested_packages=list(options.pop(\"requested_packages\", []) or []),\n imported_modules=list(options.pop(\"imported_modules\", []) or []),\n options=dict(options),\n )\n frame = inspect.currentframe()\n caller_globals = frame.f_back.f_globals if frame and frame.f_back else None\n if isinstance(caller_globals, dict):\n caller_globals[\"__plat_browser_server_definition__\"] = definition\n caller_globals.setdefault(\"client_side_server\", definition)\n return definition\n\n\ndef prepare_python_source(source: str) -> BrowserPackagePlan:\n requested_packages: list[str] = []\n python_lines: list[str] = []\n for raw_line in source.splitlines():\n stripped = raw_line.strip()\n if stripped.startswith(\"!pip install \"):\n package_args = stripped[len(\"!pip install \") :].strip().split()\n requested_packages.extend(arg for arg in package_args if arg and not arg.startswith(\"-\"))\n continue\n python_lines.append(raw_line)\n python_source = \"\\n\".join(python_lines)\n imported_modules = [\n module\n for module in detect_imports(python_source)\n if module not in PYTHON_BROWSER_STDLIB_MODULES and module != \"plat\" and module != \"plat_browser\"\n ]\n return BrowserPackagePlan(\n python_source=python_source,\n requested_packages=dedupe_keep_order(requested_packages),\n imported_modules=dedupe_keep_order(imported_modules),\n )\n\n\ndef detect_imports(source: str) -> list[str]:\n tree = ast.parse(source)\n modules: list[str] = []\n for node in ast.walk(tree):\n if isinstance(node, ast.Import):\n for alias in node.names:\n modules.append(alias.name.split(\".\")[0])\n elif isinstance(node, ast.ImportFrom):\n if node.module:\n modules.append(node.module.split(\".\")[0])\n return dedupe_keep_order(modules)\n\n\ndef annotation_to_schema(annotation: Any) -> dict[str, Any]:\n origin = get_origin(annotation)\n args = get_args(annotation)\n if annotation is str:\n return {\"type\": \"string\"}\n if annotation is int:\n return {\"type\": \"integer\"}\n if annotation is float:\n return {\"type\": \"number\"}\n if annotation is bool:\n return {\"type\": \"boolean\"}\n if annotation in {dict, Mapping}:\n return {\"type\": \"object\", \"additionalProperties\": True}\n if annotation in {list, tuple}:\n return {\"type\": \"array\", \"items\": {}}\n if origin is Literal:\n values = list(args)\n schema: dict[str, Any] = {\"enum\": values}\n if values:\n schema.update(annotation_to_schema(type(values[0])))\n return schema\n if origin in {list, tuple}:\n item_annotation = args[0] if args else Any\n return {\"type\": \"array\", \"items\": annotation_to_schema(item_annotation)}\n if origin in {dict, Mapping}:\n value_annotation = args[1] if len(args) > 1 else Any\n return {\"type\": \"object\", \"additionalProperties\": annotation_to_schema(value_annotation) if value_annotation is not Any else True}\n if origin is not None and str(origin) == \"typing.Union\":\n variants = [annotation_to_schema(arg) for arg in args if arg is not type(None)]\n if len(variants) == 1:\n schema = variants[0]\n if len(args) != len(variants):\n schema = {**schema, \"nullable\": True}\n return schema\n return {\"anyOf\": variants}\n if hasattr(annotation, \"__annotations__\") and isinstance(getattr(annotation, \"__annotations__\"), dict):\n return typed_mapping_schema(annotation.__annotations__)\n return {\"type\": \"object\", \"additionalProperties\": True}\n\n\ndef build_input_model_from_params(params: list[inspect.Parameter]) -> Any:\n if not params:\n return None\n if len(params) == 1 and params[0].name in LEGACY_PAYLOAD_PARAM_NAMES:\n return params[0].annotation\n\n properties: dict[str, Any] = {}\n required: list[str] = []\n for param in params:\n if param.kind is inspect.Parameter.VAR_POSITIONAL:\n continue\n if param.kind is inspect.Parameter.VAR_KEYWORD:\n return {\n \"type\": \"object\",\n \"properties\": properties,\n \"required\": required,\n \"additionalProperties\": True,\n }\n annotation = param.annotation\n properties[param.name] = annotation_to_schema(annotation) if annotation is not inspect.Signature.empty else {\"type\": \"object\", \"additionalProperties\": True}\n if param.default is inspect.Signature.empty:\n required.append(param.name)\n\n return {\n \"type\": \"object\",\n \"properties\": properties,\n \"required\": required,\n }\n\n\ndef typed_mapping_schema(annotations: Mapping[str, Any]) -> dict[str, Any]:\n properties: dict[str, Any] = {}\n required: list[str] = []\n for key, value in annotations.items():\n properties[key] = annotation_to_schema(value)\n required.append(key)\n return {\"type\": \"object\", \"properties\": properties, \"required\": required}\n\n\ndef dedupe_keep_order(values: list[str]) -> list[str]:\n seen: set[str] = set()\n deduped: list[str] = []\n for value in values:\n if value in seen:\n continue\n seen.add(value)\n deduped.append(value)\n return deduped\n\n\ndef trim_none(value: Any) -> Any:\n if isinstance(value, dict):\n return {key: trim_none(inner) for key, inner in value.items() if inner is not None}\n if isinstance(value, list):\n return [trim_none(item) for item in value]\n return value\n\n\ndef coerce_str(value: Any) -> str | None:\n return value if isinstance(value, str) else None\n\n\ndef cast_undecorated_mode(value: Any, fallback: UndecoratedMode) -> UndecoratedMode:\n if value in {\"GET\", \"POST\", \"private\"}:\n return value\n return fallback\n\n\ndef normalize_server_message(message: Any) -> dict[str, Any]:\n if isinstance(message, dict):\n return message\n if isinstance(message, Mapping):\n return dict(message)\n\n to_py = getattr(message, \"to_py\", None)\n if callable(to_py):\n try:\n converted = to_py()\n except TypeError:\n converted = to_py(depth=-1)\n return normalize_server_message(converted)\n\n keys = (\"operationId\", \"method\", \"path\", \"input\", \"body\", \"params\", \"headers\")\n normalized: dict[str, Any] = {}\n for key in keys:\n value = None\n if hasattr(message, key):\n value = getattr(message, key)\n else:\n try:\n value = message[key]\n except Exception:\n value = None\n if value is not None:\n normalized[key] = value\n return normalized\n",
10
+ "plat_browser/types.py": "from __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any, Literal\n\n\nHttpMethod = Literal[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]\n\n\n@dataclass\nclass RouteMeta:\n name: str\n method: HttpMethod | None = None\n path: str | None = None\n decorator_path: str | None = None\n auth: str | None = None\n summary: str | None = None\n description: str | None = None\n rate_limit: Any = None\n token_limit: Any = None\n cache: Any = None\n opts: dict[str, Any] | None = None\n input_model: Any = None\n output_model: Any = None\n\n\n@dataclass\nclass ControllerMeta:\n base_path: str = \"\"\n tag: str | None = None\n auth: str | None = None\n rate_limit: Any = None\n token_limit: Any = None\n cache: Any = None\n routes: dict[str, RouteMeta] = field(default_factory=dict)\n\n\n@dataclass\nclass RouteContext:\n method: str | None = None\n url: str | None = None\n headers: dict[str, str] = field(default_factory=dict)\n auth: dict[str, Any] | None = None\n rate_limit: Any = None\n token_limit: Any = None\n cache: Any = None\n opts: dict[str, Any] | None = None\n call: Any = None\n rpc: Any = None\n request: Any = None\n",
11
+ "tests/test_plat_browser.py": "from __future__ import annotations\n\nimport asyncio\nimport textwrap\nimport unittest\n\nfrom plat_browser import (\n BrowserPLATServer,\n BucketConfig,\n Controller,\n HttpError,\n POST,\n RouteContext,\n connect_client_side_server,\n prepare_python_source,\n run_python_client_source,\n serve_client_side_server,\n)\nfrom plat_browser.client import _set_browser_client_bridge\n\n\nclass PlatBrowserTests(unittest.TestCase):\n def setUp(self) -> None:\n async def missing_connect(_: str):\n raise RuntimeError(\"connect bridge not installed\")\n\n async def missing_call(_: int, __: str, ___):\n raise RuntimeError(\"call bridge not installed\")\n\n _set_browser_client_bridge(missing_connect, missing_call)\n\n def test_prepare_python_source_hides_pip_lines_and_detects_imports(self) -> None:\n plan = prepare_python_source(\n textwrap.dedent(\n \"\"\"\n !pip install pandas fastapi\n import pandas as pd\n import json\n from plat_browser import serve_client_side_server\n from numpy.random import rand\n \"\"\"\n ).strip()\n )\n\n self.assertEqual(plan.requested_packages, [\"pandas\", \"fastapi\"])\n self.assertEqual(plan.imported_modules, [\"pandas\", \"numpy\"])\n self.assertNotIn(\"!pip install\", plan.python_source)\n\n def test_serve_client_side_server_returns_definition(self) -> None:\n class DemoApi:\n def add(self, a, b, ctx):\n return a + b\n\n definition = serve_client_side_server(\"browser-math\", [DemoApi], undecorated_mode=\"POST\")\n\n self.assertEqual(definition.server_name, \"browser-math\")\n self.assertEqual(definition.controllers, [DemoApi])\n self.assertEqual(definition.options[\"undecorated_mode\"], \"POST\")\n\n def test_browser_server_registers_undecorated_public_methods_as_post(self) -> None:\n class DemoApi:\n def add(self, a: int, b: int, ctx: RouteContext) -> int:\n return a + b\n\n def _private(self, input, ctx):\n return None\n\n server = BrowserPLATServer({}, DemoApi)\n\n self.assertIn({\"method\": \"POST\", \"path\": \"/add\", \"methodName\": \"add\"}, server.routes)\n self.assertNotIn(\"_private\", server.operations_by_id)\n self.assertEqual(server.openapi[\"paths\"][\"/add\"][\"post\"][\"operationId\"], \"add\")\n schema = server.openapi[\"paths\"][\"/add\"][\"post\"][\"requestBody\"][\"content\"][\"application/json\"][\"schema\"]\n self.assertEqual(schema[\"properties\"][\"a\"][\"type\"], \"integer\")\n self.assertEqual(schema[\"properties\"][\"b\"][\"type\"], \"integer\")\n self.assertEqual(schema[\"required\"], [\"a\", \"b\"])\n\n def test_browser_server_uses_decorated_metadata_and_docstrings(self) -> None:\n @Controller()\n class OrdersApi:\n @POST()\n def createOrder(self, input: dict[str, str], ctx: RouteContext) -> dict[str, str]:\n \"\"\"Create one order.\n\n Longer description for docs.\n \"\"\"\n\n return {\"orderId\": \"ord_123\"}\n\n server = BrowserPLATServer({}, OrdersApi)\n operation = server.openapi[\"paths\"][\"/createOrder\"][\"post\"]\n\n self.assertEqual(operation[\"summary\"], \"Create one order.\")\n self.assertIn(\"Longer description\", operation[\"description\"])\n\n def test_browser_server_dispatches_requests_and_emits_progress(self) -> None:\n class JobsApi:\n async def countTo(self, end: int, ctx: RouteContext) -> dict[str, int]:\n for index in range(1, end + 1):\n ctx.call.progress({\"current\": index, \"end\": end})\n return {\"done\": end}\n\n server = BrowserPLATServer({}, JobsApi)\n events: list[dict[str, object]] = []\n\n result = asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"countTo\",\n \"method\": \"POST\",\n \"path\": \"/countTo\",\n \"input\": {\"end\": 3},\n },\n emit=events.append,\n )\n )\n\n self.assertEqual(result, {\"done\": 3})\n self.assertEqual(\n events,\n [\n {\"event\": \"progress\", \"data\": {\"current\": 1, \"end\": 3}},\n {\"event\": \"progress\", \"data\": {\"current\": 2, \"end\": 3}},\n {\"event\": \"progress\", \"data\": {\"current\": 3, \"end\": 3}},\n ],\n )\n\n def test_browser_server_keeps_legacy_single_input_param_working(self) -> None:\n class LegacyApi:\n def add(self, input: dict[str, int], ctx: RouteContext) -> int:\n return input[\"a\"] + input[\"b\"]\n\n server = BrowserPLATServer({}, LegacyApi)\n result = asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 2, \"b\": 5},\n }\n )\n )\n self.assertEqual(result, 7)\n\n def test_browser_server_normalizes_proxy_like_request_objects(self) -> None:\n class DemoApi:\n def add(self, a: int, b: int, ctx: RouteContext) -> int:\n return a + b\n\n class FakeProxy:\n def __init__(self, payload):\n self._payload = payload\n\n def to_py(self):\n return self._payload\n\n server = BrowserPLATServer({}, DemoApi)\n result = asyncio.run(\n server.handle_request(\n FakeProxy(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 9, \"b\": 4},\n }\n )\n )\n )\n self.assertEqual(result, 13)\n\n def test_browser_server_enforces_auth_and_populates_ctx(self) -> None:\n class SecureApi:\n def secret(self, ctx: RouteContext) -> str:\n return f\"hello {ctx.auth['sub']}\"\n\n async def verify(mode, request, ctx):\n self.assertEqual(mode, \"user\")\n token = request.get(\"headers\", {}).get(\"authorization\")\n if token != \"Bearer good\":\n raise HttpError(401, \"Missing or invalid authorization token\")\n return {\"sub\": \"demo-user\"}\n\n server = BrowserPLATServer(\n {\n \"auth\": {\"verify\": verify},\n \"default_auth\": \"user\",\n },\n SecureApi,\n )\n\n result = asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"secret\",\n \"method\": \"POST\",\n \"path\": \"/secret\",\n \"headers\": {\"authorization\": \"Bearer good\"},\n }\n )\n )\n\n self.assertEqual(result, \"hello demo-user\")\n with self.assertRaises(HttpError) as error:\n asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"secret\",\n \"method\": \"POST\",\n \"path\": \"/secret\",\n \"headers\": {},\n }\n )\n )\n self.assertEqual(error.exception.status_code, 401)\n\n def test_browser_server_supports_cache(self) -> None:\n calls = 0\n\n class DemoApi:\n def add(self, a: int, b: int, ctx: RouteContext) -> int:\n nonlocal calls\n calls += 1\n return a + b\n\n server = BrowserPLATServer(\n {\n \"cache\": {\n \"controller\": None,\n },\n },\n DemoApi,\n )\n operation = server.operations_by_id[\"add\"]\n operation.route_meta.cache = {\"key\": \"sum:{a}:{b}\", \"methods\": [\"POST\"], \"ttl\": 60}\n\n first = asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 1, \"b\": 4},\n }\n )\n )\n second = asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 1, \"b\": 4},\n }\n )\n )\n\n self.assertEqual(first, 5)\n self.assertEqual(second, 5)\n self.assertEqual(calls, 1)\n\n def test_browser_server_supports_rate_limit(self) -> None:\n class DemoApi:\n def add(self, a: int, b: int, ctx: RouteContext) -> int:\n return a + b\n\n server = BrowserPLATServer(\n {\n \"rate_limit\": {\n \"configs\": {\n \"add\": BucketConfig(max_balance=1, fill_interval=60_000, fill_amount=1),\n },\n },\n },\n DemoApi,\n )\n operation = server.operations_by_id[\"add\"]\n operation.route_meta.rate_limit = {\"key\": \"add\", \"cost\": 1}\n\n self.assertEqual(\n asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 2, \"b\": 3},\n }\n )\n ),\n 5,\n )\n with self.assertRaises(HttpError) as error:\n asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 2, \"b\": 3},\n }\n )\n )\n self.assertEqual(error.exception.status_code, 429)\n\n def test_browser_server_supports_token_limit(self) -> None:\n class DemoApi:\n def add(self, a: int, b: int, ctx: RouteContext) -> int:\n return a + b\n\n server = BrowserPLATServer(\n {\n \"token_limit\": {\n \"configs\": {\n \"tokens\": BucketConfig(max_balance=2, fill_interval=60_000, fill_amount=1),\n },\n },\n },\n DemoApi,\n )\n operation = server.operations_by_id[\"add\"]\n operation.route_meta.token_limit = {\"key\": \"tokens\", \"call_cost\": 2}\n\n self.assertEqual(\n asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 10, \"b\": 5},\n }\n )\n ),\n 15,\n )\n with self.assertRaises(HttpError) as error:\n asyncio.run(\n server.handle_request(\n {\n \"operationId\": \"add\",\n \"method\": \"POST\",\n \"path\": \"/add\",\n \"input\": {\"a\": 10, \"b\": 5},\n }\n )\n )\n self.assertEqual(error.exception.status_code, 429)\n\n def test_browser_python_client_connects_and_calls_with_kwargs(self) -> None:\n async def fake_connect(base_url: str, options=None):\n return {\"client_id\": 7, \"base_url\": base_url, \"openapi\": {\"paths\": {\"/add\": {\"post\": {}}}}}\n\n async def fake_call(client_id: int, method_name: str, payload):\n self.assertEqual(client_id, 7)\n self.assertEqual(method_name, \"add\")\n self.assertEqual(payload, {\"a\": 20, \"b\": 22})\n return 42\n\n _set_browser_client_bridge(fake_connect, fake_call)\n\n async def scenario():\n client = await connect_client_side_server(\"css://browser-python-math\")\n return await client.add(a=20, b=22)\n\n self.assertEqual(asyncio.run(scenario()), 42)\n\n def test_run_python_client_source_returns_last_expression(self) -> None:\n async def fake_connect(base_url: str, options=None):\n return {\"client_id\": 11, \"base_url\": base_url}\n\n async def fake_call(client_id: int, method_name: str, payload):\n self.assertEqual(client_id, 11)\n self.assertEqual(method_name, \"add\")\n self.assertEqual(payload, {\"a\": 3, \"b\": 9})\n return 12\n\n _set_browser_client_bridge(fake_connect, fake_call)\n\n result = asyncio.run(\n run_python_client_source(\n \"\"\"\nfrom plat_browser import connect_client_side_server\n\nclient = await connect_client_side_server(\"css://browser-python-math\")\nawait client.add(a=3, b=9)\n\"\"\".strip()\n )\n )\n self.assertEqual(result, 12)\n"
12
+ };
13
+ //# sourceMappingURL=python-browser-sources.js.map