coco-framework 0.1.0__py3-none-any.whl

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 (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,706 @@
1
+ """Adapted from https://github.com/noahmorrison/chevron.
2
+
3
+ MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from collections.abc import Iterator, Mapping, Sequence
10
+ from types import MappingProxyType
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Literal,
15
+ cast,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from typing import TypeAlias
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ Scopes: TypeAlias = list[Literal[False, 0] | Mapping[str, Any]]
25
+
26
+
27
+ # Globals
28
+ _CURRENT_LINE = 1
29
+ _LAST_TAG_LINE = None
30
+
31
+
32
+ class ChevronError(SyntaxError):
33
+ """Custom exception for Chevron errors."""
34
+
35
+
36
+ #
37
+ # Helper functions
38
+ #
39
+
40
+
41
+ def grab_literal(template: str, l_del: str) -> tuple[str, str]:
42
+ """Parse a literal from the template.
43
+
44
+ Args:
45
+ template: The template to parse.
46
+ l_del: The left delimiter.
47
+
48
+ Returns:
49
+ The literal and the template.
50
+ """
51
+ global _CURRENT_LINE
52
+
53
+ try:
54
+ # Look for the next tag and move the template to it
55
+ literal, template = template.split(l_del, 1)
56
+ _CURRENT_LINE += literal.count("\n")
57
+
58
+ # There are no more tags in the template?
59
+ except ValueError:
60
+ # Then the rest of the template is a literal
61
+ return (template, "")
62
+
63
+ return (literal, template)
64
+
65
+
66
+ def l_sa_check(
67
+ template: str, # noqa: ARG001
68
+ literal: str,
69
+ is_standalone: bool, # noqa: FBT001
70
+ ) -> bool:
71
+ """Do a preliminary check to see if a tag could be a standalone.
72
+
73
+ Args:
74
+ template: The template. (Not used.)
75
+ literal: The literal.
76
+ is_standalone: Whether the tag is standalone.
77
+
78
+ Returns:
79
+ Whether the tag could be a standalone.
80
+ """
81
+ # If there is a newline, or the previous tag was a standalone
82
+ if literal.find("\n") != -1 or is_standalone:
83
+ padding = literal.rsplit("\n", maxsplit=1)[-1]
84
+
85
+ # If all the characters since the last newline are spaces
86
+ # Then the next tag could be a standalone
87
+ # Otherwise it can't be
88
+ return padding.isspace() or not padding
89
+ return False
90
+
91
+
92
+ def r_sa_check(
93
+ template: str,
94
+ tag_type: str,
95
+ is_standalone: bool, # noqa: FBT001
96
+ ) -> bool:
97
+ """Do a final check to see if a tag could be a standalone.
98
+
99
+ Args:
100
+ template: The template.
101
+ tag_type: The type of the tag.
102
+ is_standalone: Whether the tag is standalone.
103
+
104
+ Returns:
105
+ Whether the tag could be a standalone.
106
+ """
107
+ # Check right side if we might be a standalone
108
+ if is_standalone and tag_type not in {"variable", "no escape"}:
109
+ on_newline = template.split("\n", 1)
110
+
111
+ # If the stuff to the right of us are spaces we're a standalone
112
+ return on_newline[0].isspace() or not on_newline[0]
113
+
114
+ # If we're a tag can't be a standalone
115
+ return False
116
+
117
+
118
+ def parse_tag(template: str, l_del: str, r_del: str) -> tuple[tuple[str, str], str]:
119
+ """Parse a tag from a template.
120
+
121
+ Args:
122
+ template: The template.
123
+ l_del: The left delimiter.
124
+ r_del: The right delimiter.
125
+
126
+ Returns:
127
+ The tag and the template.
128
+
129
+ Raises:
130
+ ChevronError: If the tag is unclosed.
131
+ ChevronError: If the set delimiter tag is unclosed.
132
+ """
133
+ tag_types = {
134
+ "!": "comment",
135
+ "#": "section",
136
+ "^": "inverted section",
137
+ "/": "end",
138
+ ">": "partial",
139
+ "=": "set delimiter?",
140
+ "{": "no escape?",
141
+ "&": "no escape",
142
+ }
143
+
144
+ # Get the tag
145
+ try:
146
+ tag, template = template.split(r_del, 1)
147
+ except ValueError as e:
148
+ msg = f"unclosed tag at line {_CURRENT_LINE}"
149
+ raise ChevronError(msg) from e
150
+
151
+ # Check for empty tags
152
+ if not tag.strip():
153
+ msg = f"empty tag at line {_CURRENT_LINE}"
154
+ raise ChevronError(msg)
155
+
156
+ # Find the type meaning of the first character
157
+ tag_type = tag_types.get(tag[0], "variable")
158
+
159
+ # If the type is not a variable
160
+ if tag_type != "variable":
161
+ # Then that first character is not needed
162
+ tag = tag[1:]
163
+
164
+ # If we might be a set delimiter tag
165
+ if tag_type == "set delimiter?":
166
+ # Double check to make sure we are
167
+ if tag.endswith("="):
168
+ tag_type = "set delimiter"
169
+ # Remove the equal sign
170
+ tag = tag[:-1]
171
+
172
+ # Otherwise we should complain
173
+ else:
174
+ msg = f"unclosed set delimiter tag\nat line {_CURRENT_LINE}"
175
+ raise ChevronError(msg)
176
+
177
+ elif (
178
+ # If we might be a no html escape tag
179
+ tag_type == "no escape?"
180
+ # And we have a third curly brace
181
+ # (And are using curly braces as delimiters)
182
+ and l_del == "{{"
183
+ and r_del == "}}"
184
+ and template.startswith("}")
185
+ ):
186
+ # Then we are a no html escape tag
187
+ template = template[1:]
188
+ tag_type = "no escape"
189
+
190
+ # Strip the whitespace off the key and return
191
+ return ((tag_type, tag.strip()), template)
192
+
193
+
194
+ #
195
+ # The main tokenizing function
196
+ #
197
+
198
+
199
+ def tokenize(
200
+ template: str, def_ldel: str = "{{", def_rdel: str = "}}"
201
+ ) -> Iterator[tuple[str, str]]:
202
+ """Tokenize a mustache template.
203
+
204
+ Tokenizes a mustache template in a generator fashion, using file-like objects. It
205
+ also accepts a string containing the template.
206
+
207
+ Args:
208
+ template: a file-like object, or a string of a mustache template
209
+ def_ldel: The default left delimiter
210
+ (`'{{'` by default, as in spec compliant mustache)
211
+ def_rdel: The default right delimiter
212
+ (`'}}'` by default, as in spec compliant mustache)
213
+
214
+ Yields:
215
+ Mustache tags in the form of a tuple `(tag_type, tag_key)` where `tag_type` is
216
+ one of:
217
+
218
+ * literal
219
+ * section
220
+ * inverted section
221
+ * end
222
+ * partial
223
+ * no escape
224
+
225
+ ...and `tag_key` is either the key or in the case of a literal tag, the
226
+ literal itself.
227
+
228
+ Raises:
229
+ ChevronError: If there is a syntax error in the template.
230
+ """
231
+ global _CURRENT_LINE, _LAST_TAG_LINE
232
+ _CURRENT_LINE = 1
233
+ _LAST_TAG_LINE = None
234
+
235
+ is_standalone = True
236
+ open_sections = []
237
+ l_del = def_ldel
238
+ r_del = def_rdel
239
+
240
+ while template:
241
+ literal, template = grab_literal(template, l_del)
242
+
243
+ # If the template is completed
244
+ if not template:
245
+ # Then yield the literal and leave
246
+ yield ("literal", literal)
247
+ break
248
+
249
+ # Do the first check to see if we could be a standalone
250
+ is_standalone = l_sa_check(template, literal, is_standalone)
251
+
252
+ # Parse the tag
253
+ tag, template = parse_tag(template, l_del, r_del)
254
+ tag_type, tag_key = tag
255
+
256
+ # Special tag logic
257
+
258
+ # If we are a set delimiter tag
259
+ if tag_type == "set delimiter":
260
+ # Then get and set the delimiters
261
+ dels = tag_key.strip().split(" ")
262
+ l_del, r_del = dels[0], dels[-1]
263
+
264
+ # If we are a section tag
265
+ elif tag_type in {"section", "inverted section"}:
266
+ # Then open a new section
267
+ open_sections.append(tag_key)
268
+ _LAST_TAG_LINE = _CURRENT_LINE
269
+
270
+ # If we are an end tag
271
+ elif tag_type == "end":
272
+ # Then check to see if the last opened section
273
+ # is the same as us
274
+ try:
275
+ last_section = open_sections.pop()
276
+ except IndexError as e:
277
+ msg = (
278
+ f'Trying to close tag "{tag_key}"\n'
279
+ "Looks like it was not opened.\n"
280
+ f"line {_CURRENT_LINE + 1}"
281
+ )
282
+ raise ChevronError(msg) from e
283
+ if tag_key != last_section:
284
+ # Otherwise we need to complain
285
+ msg = (
286
+ f'Trying to close tag "{tag_key}"\n'
287
+ f'last open tag is "{last_section}"\n'
288
+ f"line {_CURRENT_LINE + 1}"
289
+ )
290
+ raise ChevronError(msg)
291
+
292
+ # Do the second check to see if we're a standalone
293
+ is_standalone = r_sa_check(template, tag_type, is_standalone)
294
+
295
+ # Which if we are
296
+ if is_standalone:
297
+ # Remove the stuff before the newline
298
+ template = template.split("\n", 1)[-1]
299
+
300
+ # Partials need to keep the spaces on their left
301
+ if tag_type != "partial":
302
+ # But other tags don't
303
+ literal = literal.rstrip(" ")
304
+
305
+ # Start yielding
306
+ # Ignore literals that are empty
307
+ if literal:
308
+ yield ("literal", literal)
309
+
310
+ # Ignore comments and set delimiters
311
+ if tag_type not in {"comment", "set delimiter?"}:
312
+ yield (tag_type, tag_key)
313
+
314
+ # If there are any open sections when we're done
315
+ if open_sections:
316
+ # Then we need to complain
317
+ msg = (
318
+ "Unexpected EOF\n"
319
+ f'the tag "{open_sections[-1]}" was never closed\n'
320
+ f"was opened at line {_LAST_TAG_LINE}"
321
+ )
322
+ raise ChevronError(msg)
323
+
324
+
325
+ #
326
+ # Helper functions
327
+ #
328
+
329
+
330
+ def _html_escape(string: str) -> str:
331
+ """Return the HTML-escaped string with these characters escaped: `" & < >`."""
332
+ html_codes = {
333
+ '"': "&quot;",
334
+ "<": "&lt;",
335
+ ">": "&gt;",
336
+ }
337
+
338
+ # & must be handled first
339
+ string = string.replace("&", "&amp;")
340
+ for char, code in html_codes.items():
341
+ string = string.replace(char, code)
342
+ return string
343
+
344
+
345
+ def _get_key(
346
+ key: str,
347
+ scopes: Scopes,
348
+ *,
349
+ warn: bool,
350
+ keep: bool,
351
+ def_ldel: str,
352
+ def_rdel: str,
353
+ ) -> Any:
354
+ """Retrieve a value from the current scope using a dot-separated key path.
355
+
356
+ Traverses through nested dictionaries and lists using dot notation.
357
+
358
+ Supports special key `'.'` to return the current scope.
359
+
360
+ Args:
361
+ key: Dot-separated key path (e.g., `'user.name'` or `'.'` for current scope).
362
+ scopes: List of scope dictionaries to search through.
363
+ warn: Whether to log a warning when a key is not found.
364
+ keep: Whether to return the original template tag when key is not found.
365
+ def_ldel: Left delimiter for template (used when keep is `True`).
366
+ def_rdel: Right delimiter for template (used when keep is `True`).
367
+
368
+ Returns:
369
+ The value found at the key path.
370
+
371
+ If not found, returns the original template tag when keep is `True`,
372
+ otherwise returns an empty string.
373
+ """
374
+ # If the key is a dot
375
+ if key == ".":
376
+ # Then just return the current scope
377
+ return scopes[0]
378
+
379
+ # Loop through the scopes
380
+ for scope in scopes:
381
+ try:
382
+ # Return an empty string if falsy, with two exceptions
383
+ # 0 should return 0, and False should return False
384
+ if scope in (0, False):
385
+ return scope
386
+
387
+ resolved_scope: Literal[False, 0] | Mapping[str, Any] | Sequence[Any] = (
388
+ scope
389
+ )
390
+ # For every dot separated key
391
+ for child in key.split("."):
392
+ # Return an empty string if falsy, with two exceptions
393
+ # 0 should return 0, and False should return False
394
+ if resolved_scope in (0, False):
395
+ return resolved_scope
396
+ # Move into the scope
397
+ if isinstance(resolved_scope, Mapping):
398
+ try:
399
+ resolved_scope = resolved_scope[child]
400
+ except (KeyError, TypeError):
401
+ # Key not found - will be caught by outer try-except
402
+ msg = f"Key {child!r} not found in dict"
403
+ raise KeyError(msg) from None
404
+ elif isinstance(resolved_scope, (list, tuple)):
405
+ try:
406
+ resolved_scope = resolved_scope[int(child)]
407
+ except (ValueError, IndexError, TypeError):
408
+ # Invalid index - will be caught by outer try-except
409
+ msg = f"Invalid index {child!r} for list/tuple"
410
+ raise IndexError(msg) from None
411
+ else:
412
+ # Reject everything else for security
413
+ # This prevents traversing into arbitrary Python objects
414
+ msg = (
415
+ f"Cannot traverse into {type(resolved_scope).__name__}. "
416
+ "Mustache templates only support dict, list, and tuple. "
417
+ f"Got: {type(resolved_scope)}"
418
+ )
419
+ raise TypeError(msg) # noqa: TRY301
420
+
421
+ try:
422
+ # This allows for custom falsy data types
423
+ # https://github.com/noahmorrison/chevron/issues/35
424
+ if resolved_scope._CHEVRON_return_scope_when_falsy: # type: ignore[union-attr] # noqa: SLF001
425
+ return resolved_scope
426
+ except AttributeError:
427
+ if resolved_scope in (0, False):
428
+ return resolved_scope
429
+ return resolved_scope or ""
430
+ except (AttributeError, KeyError, IndexError, ValueError, TypeError):
431
+ # We couldn't find the key in the current scope
432
+ # TypeError: Attempted to traverse into non-dict/list type
433
+ # We'll try again on the next pass
434
+ pass
435
+
436
+ # We couldn't find the key in any of the scopes
437
+
438
+ if warn:
439
+ logger.warning("Could not find key '%s'", key)
440
+
441
+ if keep:
442
+ return f"{def_ldel} {key} {def_rdel}"
443
+
444
+ return ""
445
+
446
+
447
+ def _get_partial(name: str, partials_dict: Mapping[str, str]) -> str:
448
+ """Load a partial.
449
+
450
+ Returns:
451
+ The partial.
452
+ """
453
+ try:
454
+ # Maybe the partial is in the dictionary
455
+ return partials_dict[name]
456
+ except KeyError:
457
+ return ""
458
+
459
+
460
+ #
461
+ # The main rendering function
462
+ #
463
+ g_token_cache: dict[str, list[tuple[str, str]]] = {}
464
+
465
+ EMPTY_DICT: MappingProxyType[str, str] = MappingProxyType({})
466
+
467
+
468
+ def render(
469
+ template: str | list[tuple[str, str]] = "",
470
+ data: Mapping[str, Any] = EMPTY_DICT,
471
+ partials_dict: Mapping[str, str] = EMPTY_DICT,
472
+ padding: str = "",
473
+ def_ldel: str = "{{",
474
+ def_rdel: str = "}}",
475
+ scopes: Scopes | None = None,
476
+ warn: bool = False, # noqa: FBT001,FBT002
477
+ keep: bool = False, # noqa: FBT001,FBT002
478
+ ) -> str:
479
+ """Render a mustache template.
480
+
481
+ Renders a mustache template with a data scope and inline partial capability.
482
+
483
+ Args:
484
+ template: A file-like object or a string containing the template.
485
+ data: A python dictionary with your data scope.
486
+ partials_dict: A python dictionary which will be search for partials
487
+ before the filesystem is.
488
+
489
+ `{'include': 'foo'}` is the same as a file called include.mustache
490
+ (defaults to `{}`).
491
+ padding: This is for padding partials, and shouldn't be used
492
+ (but can be if you really want to).
493
+ def_ldel: The default left delimiter
494
+
495
+ (`'{{'` by default, as in spec compliant mustache).
496
+ def_rdel: The default right delimiter
497
+
498
+ (`'}}'` by default, as in spec compliant mustache).
499
+ scopes: The list of scopes that `get_key` will look through.
500
+ warn: Log a warning when a template substitution isn't found in the data
501
+ keep: Keep unreplaced tags when a substitution isn't found in the data.
502
+
503
+ Returns:
504
+ A string containing the rendered template.
505
+ """
506
+ # If the template is a sequence but not derived from a string
507
+ if isinstance(template, Sequence) and not isinstance(template, str):
508
+ # Then we don't need to tokenize it
509
+ # But it does need to be a generator
510
+ tokens: Iterator[tuple[str, str]] = (token for token in template)
511
+ elif template in g_token_cache:
512
+ tokens = (token for token in g_token_cache[template])
513
+ else:
514
+ # Otherwise make a generator
515
+ tokens = tokenize(template, def_ldel, def_rdel)
516
+
517
+ output = ""
518
+
519
+ if scopes is None:
520
+ scopes = [data]
521
+
522
+ # Run through the tokens
523
+ for tag, key in tokens:
524
+ # Set the current scope
525
+ current_scope = scopes[0]
526
+
527
+ # If we're an end tag
528
+ if tag == "end":
529
+ # Pop out of the latest scope
530
+ del scopes[0]
531
+
532
+ # If the current scope is falsy and not the only scope
533
+ elif not current_scope and len(scopes) != 1:
534
+ if tag in {"section", "inverted section"}:
535
+ # Set the most recent scope to a falsy value
536
+ scopes.insert(0, False)
537
+
538
+ # If we're a literal tag
539
+ elif tag == "literal":
540
+ # Add padding to the key and add it to the output
541
+ output += key.replace("\n", "\n" + padding)
542
+
543
+ # If we're a variable tag
544
+ elif tag == "variable":
545
+ # Add the html escaped key to the output
546
+ thing = _get_key(
547
+ key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
548
+ )
549
+ if thing is True and key == ".":
550
+ # if we've coerced into a boolean by accident
551
+ # (inverted tags do this)
552
+ # then get the un-coerced object (next in the stack)
553
+ thing = scopes[1]
554
+ if not isinstance(thing, str):
555
+ thing = str(thing)
556
+ output += _html_escape(thing)
557
+
558
+ # If we're a no html escape tag
559
+ elif tag == "no escape":
560
+ # Just lookup the key and add it
561
+ thing = _get_key(
562
+ key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
563
+ )
564
+ if not isinstance(thing, str):
565
+ thing = str(thing)
566
+ output += thing
567
+
568
+ # If we're a section tag
569
+ elif tag == "section":
570
+ # Get the sections scope
571
+ scope = _get_key(
572
+ key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
573
+ )
574
+
575
+ # If the scope is a callable (as described in
576
+ # https://mustache.github.io/mustache.5.html)
577
+ if callable(scope):
578
+ # Generate template text from tags
579
+ text = ""
580
+ tags: list[tuple[str, str]] = []
581
+ for token in tokens:
582
+ if token == ("end", key):
583
+ break
584
+
585
+ tags.append(token)
586
+ tag_type, tag_key = token
587
+ if tag_type == "literal":
588
+ text += tag_key
589
+ elif tag_type == "no escape":
590
+ text += f"{def_ldel}& {tag_key} {def_rdel}"
591
+ else:
592
+ text += "{}{} {}{}".format(
593
+ def_ldel,
594
+ {
595
+ "comment": "!",
596
+ "section": "#",
597
+ "inverted section": "^",
598
+ "end": "/",
599
+ "partial": ">",
600
+ "set delimiter": "=",
601
+ "no escape": "&",
602
+ "variable": "",
603
+ }[tag_type],
604
+ tag_key,
605
+ def_rdel,
606
+ )
607
+
608
+ g_token_cache[text] = tags
609
+
610
+ rend = scope(
611
+ text,
612
+ lambda template, data=None: render(
613
+ template,
614
+ data={},
615
+ partials_dict=partials_dict,
616
+ padding=padding,
617
+ def_ldel=def_ldel,
618
+ def_rdel=def_rdel,
619
+ scopes=(data and [data, *scopes]) or scopes,
620
+ warn=warn,
621
+ keep=keep,
622
+ ),
623
+ )
624
+
625
+ output += rend
626
+
627
+ # If the scope is a sequence, an iterator or generator but not
628
+ # derived from a string
629
+ elif isinstance(scope, (Sequence, Iterator)) and not isinstance(scope, str):
630
+ # Then we need to do some looping
631
+
632
+ # Gather up all the tags inside the section
633
+ # (And don't be tricked by nested end tags with the same key)
634
+ # TODO: This feels like it still has edge cases, no?
635
+ tags = []
636
+ tags_with_same_key = 0
637
+ for token in tokens:
638
+ if token == ("section", key):
639
+ tags_with_same_key += 1
640
+ if token == ("end", key):
641
+ tags_with_same_key -= 1
642
+ if tags_with_same_key < 0:
643
+ break
644
+ tags.append(token)
645
+
646
+ # For every item in the scope
647
+ for thing in scope:
648
+ # Append it as the most recent scope and render
649
+ new_scope = [thing, *scopes]
650
+ rend = render(
651
+ template=tags,
652
+ scopes=new_scope,
653
+ padding=padding,
654
+ partials_dict=partials_dict,
655
+ def_ldel=def_ldel,
656
+ def_rdel=def_rdel,
657
+ warn=warn,
658
+ keep=keep,
659
+ )
660
+
661
+ output += rend
662
+
663
+ else:
664
+ # Otherwise we're just a scope section
665
+ scopes.insert(0, scope)
666
+
667
+ # If we're an inverted section
668
+ elif tag == "inverted section":
669
+ # Add the flipped scope to the scopes
670
+ scope = _get_key(
671
+ key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
672
+ )
673
+ scopes.insert(0, cast("Literal[False]", not scope))
674
+
675
+ # If we're a partial
676
+ elif tag == "partial":
677
+ # Load the partial
678
+ partial = _get_partial(key, partials_dict)
679
+
680
+ # Find what to pad the partial with
681
+ left = output.rpartition("\n")[2]
682
+ part_padding = padding
683
+ if left.isspace():
684
+ part_padding += left
685
+
686
+ # Render the partial
687
+ part_out = render(
688
+ template=partial,
689
+ partials_dict=partials_dict,
690
+ def_ldel=def_ldel,
691
+ def_rdel=def_rdel,
692
+ padding=part_padding,
693
+ scopes=scopes,
694
+ warn=warn,
695
+ keep=keep,
696
+ )
697
+
698
+ # If the partial was indented
699
+ if left.isspace():
700
+ # then remove the spaces from the end
701
+ part_out = part_out.rstrip(" \t")
702
+
703
+ # Add the partials output to the output
704
+ output += part_out
705
+
706
+ return output