mirascope 2.0.0__py3-none-any.whl → 2.0.0a0__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 (442) hide show
  1. mirascope/__init__.py +2 -11
  2. mirascope/graphs/__init__.py +22 -0
  3. mirascope/graphs/finite_state_machine.py +625 -0
  4. mirascope/llm/__init__.py +16 -101
  5. mirascope/llm/agents/__init__.py +15 -0
  6. mirascope/llm/agents/agent.py +97 -0
  7. mirascope/llm/agents/agent_template.py +45 -0
  8. mirascope/llm/agents/decorator.py +176 -0
  9. mirascope/llm/calls/__init__.py +1 -2
  10. mirascope/llm/calls/base_call.py +33 -0
  11. mirascope/llm/calls/calls.py +58 -84
  12. mirascope/llm/calls/decorator.py +120 -140
  13. mirascope/llm/clients/__init__.py +34 -0
  14. mirascope/llm/clients/anthropic/__init__.py +11 -0
  15. mirascope/llm/{providers/openai/completions → clients/anthropic}/_utils/__init__.py +0 -2
  16. mirascope/llm/{providers → clients}/anthropic/_utils/decode.py +22 -66
  17. mirascope/llm/clients/anthropic/_utils/encode.py +243 -0
  18. mirascope/llm/clients/anthropic/clients.py +819 -0
  19. mirascope/llm/clients/anthropic/model_ids.py +8 -0
  20. mirascope/llm/{providers → clients}/base/__init__.py +5 -4
  21. mirascope/llm/{providers → clients}/base/_utils.py +17 -78
  22. mirascope/llm/{providers/base/base_provider.py → clients/base/client.py} +145 -468
  23. mirascope/llm/{models → clients/base}/params.py +37 -16
  24. mirascope/llm/clients/google/__init__.py +6 -0
  25. mirascope/llm/{providers/openai/responses → clients/google}/_utils/__init__.py +0 -2
  26. mirascope/llm/{providers → clients}/google/_utils/decode.py +22 -98
  27. mirascope/llm/{providers → clients}/google/_utils/encode.py +46 -168
  28. mirascope/llm/clients/google/clients.py +853 -0
  29. mirascope/llm/clients/google/model_ids.py +15 -0
  30. mirascope/llm/clients/openai/__init__.py +25 -0
  31. mirascope/llm/clients/openai/completions/__init__.py +9 -0
  32. mirascope/llm/{providers/google → clients/openai/completions}/_utils/__init__.py +0 -4
  33. mirascope/llm/{providers → clients}/openai/completions/_utils/decode.py +9 -74
  34. mirascope/llm/{providers → clients}/openai/completions/_utils/encode.py +52 -70
  35. mirascope/llm/clients/openai/completions/_utils/model_features.py +81 -0
  36. mirascope/llm/clients/openai/completions/clients.py +833 -0
  37. mirascope/llm/clients/openai/completions/model_ids.py +8 -0
  38. mirascope/llm/clients/openai/responses/__init__.py +9 -0
  39. mirascope/llm/clients/openai/responses/_utils/__init__.py +13 -0
  40. mirascope/llm/{providers → clients}/openai/responses/_utils/decode.py +14 -80
  41. mirascope/llm/{providers → clients}/openai/responses/_utils/encode.py +41 -92
  42. mirascope/llm/clients/openai/responses/_utils/model_features.py +87 -0
  43. mirascope/llm/clients/openai/responses/clients.py +832 -0
  44. mirascope/llm/clients/openai/responses/model_ids.py +8 -0
  45. mirascope/llm/clients/openai/shared/__init__.py +7 -0
  46. mirascope/llm/clients/openai/shared/_utils.py +55 -0
  47. mirascope/llm/clients/providers.py +175 -0
  48. mirascope/llm/content/__init__.py +2 -3
  49. mirascope/llm/content/tool_call.py +0 -6
  50. mirascope/llm/content/tool_output.py +5 -22
  51. mirascope/llm/context/_utils.py +6 -19
  52. mirascope/llm/exceptions.py +43 -298
  53. mirascope/llm/formatting/__init__.py +2 -19
  54. mirascope/llm/formatting/_utils.py +74 -0
  55. mirascope/llm/formatting/format.py +30 -219
  56. mirascope/llm/formatting/from_call_args.py +2 -2
  57. mirascope/llm/formatting/partial.py +7 -80
  58. mirascope/llm/formatting/types.py +64 -21
  59. mirascope/llm/mcp/__init__.py +2 -2
  60. mirascope/llm/mcp/client.py +118 -0
  61. mirascope/llm/messages/__init__.py +0 -3
  62. mirascope/llm/messages/message.py +5 -13
  63. mirascope/llm/models/__init__.py +2 -7
  64. mirascope/llm/models/models.py +139 -315
  65. mirascope/llm/prompts/__init__.py +12 -13
  66. mirascope/llm/prompts/_utils.py +43 -14
  67. mirascope/llm/prompts/decorator.py +204 -144
  68. mirascope/llm/prompts/protocols.py +59 -25
  69. mirascope/llm/responses/__init__.py +1 -9
  70. mirascope/llm/responses/_utils.py +12 -102
  71. mirascope/llm/responses/base_response.py +6 -18
  72. mirascope/llm/responses/base_stream_response.py +50 -173
  73. mirascope/llm/responses/finish_reason.py +0 -1
  74. mirascope/llm/responses/response.py +13 -34
  75. mirascope/llm/responses/root_response.py +29 -100
  76. mirascope/llm/responses/stream_response.py +31 -40
  77. mirascope/llm/tools/__init__.py +2 -9
  78. mirascope/llm/tools/_utils.py +3 -12
  79. mirascope/llm/tools/decorator.py +16 -25
  80. mirascope/llm/tools/protocols.py +4 -4
  81. mirascope/llm/tools/tool_schema.py +19 -87
  82. mirascope/llm/tools/toolkit.py +27 -35
  83. mirascope/llm/tools/tools.py +41 -135
  84. {mirascope-2.0.0.dist-info → mirascope-2.0.0a0.dist-info}/METADATA +9 -95
  85. mirascope-2.0.0a0.dist-info/RECORD +101 -0
  86. {mirascope-2.0.0.dist-info → mirascope-2.0.0a0.dist-info}/WHEEL +1 -1
  87. {mirascope-2.0.0.dist-info → mirascope-2.0.0a0.dist-info}/licenses/LICENSE +1 -1
  88. mirascope/_stubs.py +0 -363
  89. mirascope/api/__init__.py +0 -14
  90. mirascope/api/_generated/README.md +0 -207
  91. mirascope/api/_generated/__init__.py +0 -440
  92. mirascope/api/_generated/annotations/__init__.py +0 -33
  93. mirascope/api/_generated/annotations/client.py +0 -506
  94. mirascope/api/_generated/annotations/raw_client.py +0 -1414
  95. mirascope/api/_generated/annotations/types/__init__.py +0 -31
  96. mirascope/api/_generated/annotations/types/annotations_create_request_label.py +0 -5
  97. mirascope/api/_generated/annotations/types/annotations_create_response.py +0 -48
  98. mirascope/api/_generated/annotations/types/annotations_create_response_label.py +0 -5
  99. mirascope/api/_generated/annotations/types/annotations_get_response.py +0 -48
  100. mirascope/api/_generated/annotations/types/annotations_get_response_label.py +0 -5
  101. mirascope/api/_generated/annotations/types/annotations_list_request_label.py +0 -5
  102. mirascope/api/_generated/annotations/types/annotations_list_response.py +0 -21
  103. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +0 -50
  104. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item_label.py +0 -5
  105. mirascope/api/_generated/annotations/types/annotations_update_request_label.py +0 -5
  106. mirascope/api/_generated/annotations/types/annotations_update_response.py +0 -48
  107. mirascope/api/_generated/annotations/types/annotations_update_response_label.py +0 -5
  108. mirascope/api/_generated/api_keys/__init__.py +0 -17
  109. mirascope/api/_generated/api_keys/client.py +0 -530
  110. mirascope/api/_generated/api_keys/raw_client.py +0 -1236
  111. mirascope/api/_generated/api_keys/types/__init__.py +0 -15
  112. mirascope/api/_generated/api_keys/types/api_keys_create_response.py +0 -28
  113. mirascope/api/_generated/api_keys/types/api_keys_get_response.py +0 -27
  114. mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +0 -40
  115. mirascope/api/_generated/api_keys/types/api_keys_list_response_item.py +0 -27
  116. mirascope/api/_generated/client.py +0 -211
  117. mirascope/api/_generated/core/__init__.py +0 -52
  118. mirascope/api/_generated/core/api_error.py +0 -23
  119. mirascope/api/_generated/core/client_wrapper.py +0 -46
  120. mirascope/api/_generated/core/datetime_utils.py +0 -28
  121. mirascope/api/_generated/core/file.py +0 -67
  122. mirascope/api/_generated/core/force_multipart.py +0 -16
  123. mirascope/api/_generated/core/http_client.py +0 -543
  124. mirascope/api/_generated/core/http_response.py +0 -55
  125. mirascope/api/_generated/core/jsonable_encoder.py +0 -100
  126. mirascope/api/_generated/core/pydantic_utilities.py +0 -255
  127. mirascope/api/_generated/core/query_encoder.py +0 -58
  128. mirascope/api/_generated/core/remove_none_from_dict.py +0 -11
  129. mirascope/api/_generated/core/request_options.py +0 -35
  130. mirascope/api/_generated/core/serialization.py +0 -276
  131. mirascope/api/_generated/docs/__init__.py +0 -4
  132. mirascope/api/_generated/docs/client.py +0 -91
  133. mirascope/api/_generated/docs/raw_client.py +0 -178
  134. mirascope/api/_generated/environment.py +0 -9
  135. mirascope/api/_generated/environments/__init__.py +0 -23
  136. mirascope/api/_generated/environments/client.py +0 -649
  137. mirascope/api/_generated/environments/raw_client.py +0 -1567
  138. mirascope/api/_generated/environments/types/__init__.py +0 -25
  139. mirascope/api/_generated/environments/types/environments_create_response.py +0 -24
  140. mirascope/api/_generated/environments/types/environments_get_analytics_response.py +0 -60
  141. mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +0 -24
  142. mirascope/api/_generated/environments/types/environments_get_analytics_response_top_models_item.py +0 -22
  143. mirascope/api/_generated/environments/types/environments_get_response.py +0 -24
  144. mirascope/api/_generated/environments/types/environments_list_response_item.py +0 -24
  145. mirascope/api/_generated/environments/types/environments_update_response.py +0 -24
  146. mirascope/api/_generated/errors/__init__.py +0 -25
  147. mirascope/api/_generated/errors/bad_request_error.py +0 -14
  148. mirascope/api/_generated/errors/conflict_error.py +0 -14
  149. mirascope/api/_generated/errors/forbidden_error.py +0 -11
  150. mirascope/api/_generated/errors/internal_server_error.py +0 -10
  151. mirascope/api/_generated/errors/not_found_error.py +0 -11
  152. mirascope/api/_generated/errors/payment_required_error.py +0 -15
  153. mirascope/api/_generated/errors/service_unavailable_error.py +0 -14
  154. mirascope/api/_generated/errors/too_many_requests_error.py +0 -15
  155. mirascope/api/_generated/errors/unauthorized_error.py +0 -11
  156. mirascope/api/_generated/functions/__init__.py +0 -39
  157. mirascope/api/_generated/functions/client.py +0 -647
  158. mirascope/api/_generated/functions/raw_client.py +0 -1890
  159. mirascope/api/_generated/functions/types/__init__.py +0 -53
  160. mirascope/api/_generated/functions/types/functions_create_request_dependencies_value.py +0 -20
  161. mirascope/api/_generated/functions/types/functions_create_response.py +0 -37
  162. mirascope/api/_generated/functions/types/functions_create_response_dependencies_value.py +0 -20
  163. mirascope/api/_generated/functions/types/functions_find_by_hash_response.py +0 -39
  164. mirascope/api/_generated/functions/types/functions_find_by_hash_response_dependencies_value.py +0 -20
  165. mirascope/api/_generated/functions/types/functions_get_by_env_response.py +0 -53
  166. mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +0 -22
  167. mirascope/api/_generated/functions/types/functions_get_response.py +0 -37
  168. mirascope/api/_generated/functions/types/functions_get_response_dependencies_value.py +0 -20
  169. mirascope/api/_generated/functions/types/functions_list_by_env_response.py +0 -25
  170. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +0 -56
  171. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +0 -22
  172. mirascope/api/_generated/functions/types/functions_list_response.py +0 -21
  173. mirascope/api/_generated/functions/types/functions_list_response_functions_item.py +0 -41
  174. mirascope/api/_generated/functions/types/functions_list_response_functions_item_dependencies_value.py +0 -20
  175. mirascope/api/_generated/health/__init__.py +0 -7
  176. mirascope/api/_generated/health/client.py +0 -92
  177. mirascope/api/_generated/health/raw_client.py +0 -175
  178. mirascope/api/_generated/health/types/__init__.py +0 -8
  179. mirascope/api/_generated/health/types/health_check_response.py +0 -22
  180. mirascope/api/_generated/health/types/health_check_response_status.py +0 -5
  181. mirascope/api/_generated/organization_invitations/__init__.py +0 -33
  182. mirascope/api/_generated/organization_invitations/client.py +0 -546
  183. mirascope/api/_generated/organization_invitations/raw_client.py +0 -1519
  184. mirascope/api/_generated/organization_invitations/types/__init__.py +0 -53
  185. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +0 -34
  186. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +0 -7
  187. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +0 -7
  188. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +0 -48
  189. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +0 -7
  190. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +0 -7
  191. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +0 -48
  192. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +0 -7
  193. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +0 -7
  194. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +0 -48
  195. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +0 -7
  196. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +0 -7
  197. mirascope/api/_generated/organization_memberships/__init__.py +0 -19
  198. mirascope/api/_generated/organization_memberships/client.py +0 -302
  199. mirascope/api/_generated/organization_memberships/raw_client.py +0 -736
  200. mirascope/api/_generated/organization_memberships/types/__init__.py +0 -27
  201. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +0 -33
  202. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +0 -7
  203. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +0 -7
  204. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +0 -31
  205. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +0 -7
  206. mirascope/api/_generated/organizations/__init__.py +0 -51
  207. mirascope/api/_generated/organizations/client.py +0 -869
  208. mirascope/api/_generated/organizations/raw_client.py +0 -2593
  209. mirascope/api/_generated/organizations/types/__init__.py +0 -71
  210. mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +0 -24
  211. mirascope/api/_generated/organizations/types/organizations_create_response.py +0 -26
  212. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +0 -5
  213. mirascope/api/_generated/organizations/types/organizations_get_response.py +0 -26
  214. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +0 -5
  215. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +0 -26
  216. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +0 -5
  217. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +0 -7
  218. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +0 -47
  219. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +0 -33
  220. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +0 -7
  221. mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +0 -24
  222. mirascope/api/_generated/organizations/types/organizations_subscription_response.py +0 -53
  223. mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +0 -7
  224. mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +0 -26
  225. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +0 -34
  226. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +0 -7
  227. mirascope/api/_generated/organizations/types/organizations_update_response.py +0 -26
  228. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +0 -5
  229. mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +0 -7
  230. mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +0 -35
  231. mirascope/api/_generated/project_memberships/__init__.py +0 -25
  232. mirascope/api/_generated/project_memberships/client.py +0 -437
  233. mirascope/api/_generated/project_memberships/raw_client.py +0 -1039
  234. mirascope/api/_generated/project_memberships/types/__init__.py +0 -29
  235. mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +0 -7
  236. mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +0 -35
  237. mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +0 -7
  238. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +0 -33
  239. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +0 -7
  240. mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +0 -7
  241. mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +0 -35
  242. mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +0 -7
  243. mirascope/api/_generated/projects/__init__.py +0 -7
  244. mirascope/api/_generated/projects/client.py +0 -428
  245. mirascope/api/_generated/projects/raw_client.py +0 -1302
  246. mirascope/api/_generated/projects/types/__init__.py +0 -10
  247. mirascope/api/_generated/projects/types/projects_create_response.py +0 -25
  248. mirascope/api/_generated/projects/types/projects_get_response.py +0 -25
  249. mirascope/api/_generated/projects/types/projects_list_response_item.py +0 -25
  250. mirascope/api/_generated/projects/types/projects_update_response.py +0 -25
  251. mirascope/api/_generated/reference.md +0 -4915
  252. mirascope/api/_generated/tags/__init__.py +0 -19
  253. mirascope/api/_generated/tags/client.py +0 -504
  254. mirascope/api/_generated/tags/raw_client.py +0 -1288
  255. mirascope/api/_generated/tags/types/__init__.py +0 -17
  256. mirascope/api/_generated/tags/types/tags_create_response.py +0 -41
  257. mirascope/api/_generated/tags/types/tags_get_response.py +0 -41
  258. mirascope/api/_generated/tags/types/tags_list_response.py +0 -23
  259. mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +0 -41
  260. mirascope/api/_generated/tags/types/tags_update_response.py +0 -41
  261. mirascope/api/_generated/token_cost/__init__.py +0 -7
  262. mirascope/api/_generated/token_cost/client.py +0 -160
  263. mirascope/api/_generated/token_cost/raw_client.py +0 -264
  264. mirascope/api/_generated/token_cost/types/__init__.py +0 -8
  265. mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +0 -54
  266. mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +0 -52
  267. mirascope/api/_generated/traces/__init__.py +0 -97
  268. mirascope/api/_generated/traces/client.py +0 -1103
  269. mirascope/api/_generated/traces/raw_client.py +0 -2322
  270. mirascope/api/_generated/traces/types/__init__.py +0 -155
  271. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +0 -29
  272. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +0 -27
  273. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +0 -23
  274. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +0 -38
  275. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +0 -19
  276. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +0 -22
  277. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +0 -20
  278. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +0 -29
  279. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +0 -31
  280. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +0 -23
  281. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +0 -38
  282. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +0 -19
  283. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +0 -22
  284. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +0 -22
  285. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +0 -48
  286. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +0 -23
  287. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +0 -38
  288. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +0 -19
  289. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +0 -24
  290. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +0 -22
  291. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +0 -20
  292. mirascope/api/_generated/traces/types/traces_create_response.py +0 -24
  293. mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +0 -22
  294. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +0 -60
  295. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_functions_item.py +0 -24
  296. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response_top_models_item.py +0 -22
  297. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +0 -33
  298. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +0 -88
  299. mirascope/api/_generated/traces/types/traces_get_trace_detail_response.py +0 -33
  300. mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -88
  301. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +0 -25
  302. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +0 -44
  303. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +0 -26
  304. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +0 -7
  305. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +0 -7
  306. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +0 -7
  307. mirascope/api/_generated/traces/types/traces_search_by_env_response.py +0 -26
  308. mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +0 -50
  309. mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item.py +0 -26
  310. mirascope/api/_generated/traces/types/traces_search_request_attribute_filters_item_operator.py +0 -7
  311. mirascope/api/_generated/traces/types/traces_search_request_sort_by.py +0 -7
  312. mirascope/api/_generated/traces/types/traces_search_request_sort_order.py +0 -5
  313. mirascope/api/_generated/traces/types/traces_search_response.py +0 -26
  314. mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +0 -50
  315. mirascope/api/_generated/types/__init__.py +0 -85
  316. mirascope/api/_generated/types/already_exists_error.py +0 -22
  317. mirascope/api/_generated/types/already_exists_error_tag.py +0 -5
  318. mirascope/api/_generated/types/bad_request_error_body.py +0 -50
  319. mirascope/api/_generated/types/click_house_error.py +0 -22
  320. mirascope/api/_generated/types/database_error.py +0 -22
  321. mirascope/api/_generated/types/database_error_tag.py +0 -5
  322. mirascope/api/_generated/types/date.py +0 -3
  323. mirascope/api/_generated/types/http_api_decode_error.py +0 -27
  324. mirascope/api/_generated/types/http_api_decode_error_tag.py +0 -5
  325. mirascope/api/_generated/types/immutable_resource_error.py +0 -22
  326. mirascope/api/_generated/types/internal_server_error_body.py +0 -49
  327. mirascope/api/_generated/types/issue.py +0 -38
  328. mirascope/api/_generated/types/issue_tag.py +0 -10
  329. mirascope/api/_generated/types/not_found_error_body.py +0 -22
  330. mirascope/api/_generated/types/not_found_error_tag.py +0 -5
  331. mirascope/api/_generated/types/number_from_string.py +0 -3
  332. mirascope/api/_generated/types/permission_denied_error.py +0 -22
  333. mirascope/api/_generated/types/permission_denied_error_tag.py +0 -5
  334. mirascope/api/_generated/types/plan_limit_exceeded_error.py +0 -32
  335. mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +0 -7
  336. mirascope/api/_generated/types/pricing_unavailable_error.py +0 -23
  337. mirascope/api/_generated/types/property_key.py +0 -7
  338. mirascope/api/_generated/types/property_key_key.py +0 -25
  339. mirascope/api/_generated/types/property_key_key_tag.py +0 -5
  340. mirascope/api/_generated/types/rate_limit_error.py +0 -31
  341. mirascope/api/_generated/types/rate_limit_error_tag.py +0 -5
  342. mirascope/api/_generated/types/service_unavailable_error_body.py +0 -24
  343. mirascope/api/_generated/types/service_unavailable_error_tag.py +0 -7
  344. mirascope/api/_generated/types/stripe_error.py +0 -20
  345. mirascope/api/_generated/types/subscription_past_due_error.py +0 -31
  346. mirascope/api/_generated/types/subscription_past_due_error_tag.py +0 -7
  347. mirascope/api/_generated/types/unauthorized_error_body.py +0 -21
  348. mirascope/api/_generated/types/unauthorized_error_tag.py +0 -5
  349. mirascope/api/client.py +0 -255
  350. mirascope/api/settings.py +0 -99
  351. mirascope/llm/formatting/output_parser.py +0 -178
  352. mirascope/llm/formatting/primitives.py +0 -192
  353. mirascope/llm/mcp/mcp_client.py +0 -130
  354. mirascope/llm/messages/_utils.py +0 -34
  355. mirascope/llm/models/thinking_config.py +0 -61
  356. mirascope/llm/prompts/prompts.py +0 -487
  357. mirascope/llm/providers/__init__.py +0 -62
  358. mirascope/llm/providers/anthropic/__init__.py +0 -11
  359. mirascope/llm/providers/anthropic/_utils/__init__.py +0 -27
  360. mirascope/llm/providers/anthropic/_utils/beta_decode.py +0 -282
  361. mirascope/llm/providers/anthropic/_utils/beta_encode.py +0 -266
  362. mirascope/llm/providers/anthropic/_utils/encode.py +0 -418
  363. mirascope/llm/providers/anthropic/_utils/errors.py +0 -46
  364. mirascope/llm/providers/anthropic/beta_provider.py +0 -374
  365. mirascope/llm/providers/anthropic/model_id.py +0 -23
  366. mirascope/llm/providers/anthropic/model_info.py +0 -87
  367. mirascope/llm/providers/anthropic/provider.py +0 -479
  368. mirascope/llm/providers/google/__init__.py +0 -6
  369. mirascope/llm/providers/google/_utils/errors.py +0 -50
  370. mirascope/llm/providers/google/model_id.py +0 -22
  371. mirascope/llm/providers/google/model_info.py +0 -63
  372. mirascope/llm/providers/google/provider.py +0 -492
  373. mirascope/llm/providers/mirascope/__init__.py +0 -5
  374. mirascope/llm/providers/mirascope/_utils.py +0 -73
  375. mirascope/llm/providers/mirascope/provider.py +0 -349
  376. mirascope/llm/providers/mlx/__init__.py +0 -9
  377. mirascope/llm/providers/mlx/_utils.py +0 -141
  378. mirascope/llm/providers/mlx/encoding/__init__.py +0 -8
  379. mirascope/llm/providers/mlx/encoding/base.py +0 -72
  380. mirascope/llm/providers/mlx/encoding/transformers.py +0 -150
  381. mirascope/llm/providers/mlx/mlx.py +0 -254
  382. mirascope/llm/providers/mlx/model_id.py +0 -17
  383. mirascope/llm/providers/mlx/provider.py +0 -452
  384. mirascope/llm/providers/model_id.py +0 -16
  385. mirascope/llm/providers/ollama/__init__.py +0 -7
  386. mirascope/llm/providers/ollama/provider.py +0 -71
  387. mirascope/llm/providers/openai/__init__.py +0 -15
  388. mirascope/llm/providers/openai/_utils/__init__.py +0 -5
  389. mirascope/llm/providers/openai/_utils/errors.py +0 -46
  390. mirascope/llm/providers/openai/completions/__init__.py +0 -7
  391. mirascope/llm/providers/openai/completions/base_provider.py +0 -542
  392. mirascope/llm/providers/openai/completions/provider.py +0 -22
  393. mirascope/llm/providers/openai/model_id.py +0 -31
  394. mirascope/llm/providers/openai/model_info.py +0 -303
  395. mirascope/llm/providers/openai/provider.py +0 -441
  396. mirascope/llm/providers/openai/responses/__init__.py +0 -5
  397. mirascope/llm/providers/openai/responses/provider.py +0 -513
  398. mirascope/llm/providers/provider_id.py +0 -24
  399. mirascope/llm/providers/provider_registry.py +0 -299
  400. mirascope/llm/providers/together/__init__.py +0 -7
  401. mirascope/llm/providers/together/provider.py +0 -40
  402. mirascope/llm/responses/usage.py +0 -95
  403. mirascope/ops/__init__.py +0 -111
  404. mirascope/ops/_internal/__init__.py +0 -5
  405. mirascope/ops/_internal/closure.py +0 -1169
  406. mirascope/ops/_internal/configuration.py +0 -177
  407. mirascope/ops/_internal/context.py +0 -76
  408. mirascope/ops/_internal/exporters/__init__.py +0 -26
  409. mirascope/ops/_internal/exporters/exporters.py +0 -395
  410. mirascope/ops/_internal/exporters/processors.py +0 -104
  411. mirascope/ops/_internal/exporters/types.py +0 -165
  412. mirascope/ops/_internal/exporters/utils.py +0 -29
  413. mirascope/ops/_internal/instrumentation/__init__.py +0 -8
  414. mirascope/ops/_internal/instrumentation/llm/__init__.py +0 -8
  415. mirascope/ops/_internal/instrumentation/llm/common.py +0 -530
  416. mirascope/ops/_internal/instrumentation/llm/cost.py +0 -190
  417. mirascope/ops/_internal/instrumentation/llm/encode.py +0 -238
  418. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/__init__.py +0 -38
  419. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_input_messages.py +0 -31
  420. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_output_messages.py +0 -38
  421. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_system_instructions.py +0 -18
  422. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/shared.py +0 -100
  423. mirascope/ops/_internal/instrumentation/llm/llm.py +0 -161
  424. mirascope/ops/_internal/instrumentation/llm/model.py +0 -1798
  425. mirascope/ops/_internal/instrumentation/llm/response.py +0 -521
  426. mirascope/ops/_internal/instrumentation/llm/serialize.py +0 -300
  427. mirascope/ops/_internal/propagation.py +0 -198
  428. mirascope/ops/_internal/protocols.py +0 -133
  429. mirascope/ops/_internal/session.py +0 -139
  430. mirascope/ops/_internal/spans.py +0 -232
  431. mirascope/ops/_internal/traced_calls.py +0 -375
  432. mirascope/ops/_internal/traced_functions.py +0 -523
  433. mirascope/ops/_internal/tracing.py +0 -353
  434. mirascope/ops/_internal/types.py +0 -13
  435. mirascope/ops/_internal/utils.py +0 -123
  436. mirascope/ops/_internal/versioned_calls.py +0 -512
  437. mirascope/ops/_internal/versioned_functions.py +0 -357
  438. mirascope/ops/_internal/versioning.py +0 -303
  439. mirascope/ops/exceptions.py +0 -21
  440. mirascope-2.0.0.dist-info/RECORD +0 -423
  441. /mirascope/llm/{providers → clients}/base/kwargs.py +0 -0
  442. /mirascope/llm/{providers → clients}/google/message.py +0 -0
@@ -1,1169 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ast
4
- import gc
5
- import hashlib
6
- import importlib.metadata
7
- import importlib.util
8
- import inspect
9
- import logging
10
- import os
11
- import site
12
- import subprocess
13
- import sys
14
- import tempfile
15
- from collections.abc import Callable
16
- from dataclasses import dataclass
17
- from functools import cached_property, lru_cache
18
- from pathlib import Path
19
- from textwrap import dedent
20
- from types import ModuleType
21
- from typing import Annotated, Any, TypedDict, TypeVar, cast, get_args, get_origin
22
-
23
- import libcst as cst
24
- import libcst.matchers as m
25
- from libcst import MaybeSentinel
26
- from packaging.markers import default_environment
27
- from packaging.requirements import Requirement
28
-
29
- from ..exceptions import ClosureComputationError
30
-
31
- logger = logging.getLogger(__name__)
32
-
33
- _BaseCompoundStatementT = TypeVar(
34
- "_BaseCompoundStatementT", bound=cst.BaseCompoundStatement
35
- )
36
-
37
-
38
- def _is_third_party(module: ModuleType, site_packages: set[str]) -> bool:
39
- """Returns True if the module is a third-party or standard library module."""
40
- module_file = getattr(module, "__file__", None)
41
- return (
42
- module.__name__ == "mirascope"
43
- or module.__name__.startswith("mirascope.")
44
- or module.__name__ in sys.stdlib_module_names
45
- or module_file is None
46
- or any(
47
- str(Path(module_file).resolve()).startswith(site_pkg)
48
- for site_pkg in site_packages
49
- )
50
- )
51
-
52
-
53
- class _RemoveVersionDecoratorTransformer(cst.CSTTransformer):
54
- """CST transformer to remove @ops.version and @version decorators."""
55
-
56
- @classmethod
57
- def _is_version_decorator(cls, decorator: cst.Decorator) -> bool:
58
- """Returns True if the decorator is @version or @ops.version."""
59
- decorator_node = decorator.decorator
60
-
61
- if isinstance(decorator_node, cst.Name) and decorator_node.value == "version":
62
- return True
63
- if (
64
- isinstance(decorator_node, cst.Call)
65
- and isinstance(decorator_node.func, cst.Name)
66
- and decorator_node.func.value == "version"
67
- ):
68
- return True
69
-
70
- if (
71
- isinstance(decorator_node, cst.Attribute)
72
- and isinstance(decorator_node.value, cst.Name)
73
- and decorator_node.value.value == "ops"
74
- and decorator_node.attr.value == "version"
75
- ):
76
- return True
77
- if isinstance(decorator_node, cst.Call) and isinstance(
78
- decorator_node.func, cst.Attribute
79
- ):
80
- func_attribute = decorator_node.func
81
- if (
82
- isinstance(func_attribute.value, cst.Name)
83
- and func_attribute.value.value == "ops"
84
- and func_attribute.attr.value == "version"
85
- ):
86
- return True
87
-
88
- return False
89
-
90
- def leave_FunctionDef(
91
- self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
92
- ) -> cst.FunctionDef:
93
- """Returns function definition with @version/@ops.version decorators removed."""
94
- new_decorators = [
95
- decorator
96
- for decorator in updated_node.decorators
97
- if not self._is_version_decorator(decorator)
98
- ]
99
- return updated_node.with_changes(decorators=new_decorators)
100
-
101
-
102
- class _RemoveDocstringTransformer(cst.CSTTransformer):
103
- """CST transformer to remove docstrings from functions and classes."""
104
-
105
- def __init__(self, exclude_fn_body: bool) -> None:
106
- super().__init__()
107
- self.exclude_fn_body = exclude_fn_body
108
-
109
- @staticmethod
110
- def _remove_first_docstring(
111
- node: _BaseCompoundStatementT,
112
- ) -> _BaseCompoundStatementT:
113
- """Returns the node with the first docstring removed from its body."""
114
- body = node.body
115
- stmts = list(body.body)
116
- if stmts:
117
- first_stmt = stmts[0]
118
- if m.matches(
119
- first_stmt, m.SimpleStatementLine(body=[m.Expr(value=m.SimpleString())])
120
- ):
121
- stmts.pop(0)
122
-
123
- if not stmts:
124
- stmts = [
125
- cst.Expr(
126
- value=cst.Ellipsis(
127
- lpar=[],
128
- rpar=[],
129
- ),
130
- semicolon=MaybeSentinel.DEFAULT,
131
- )
132
- ]
133
- if m.matches(node.body, m.IndentedBlock()):
134
- return node.with_changes(body=stmts[0])
135
- new_body = body.with_changes(body=stmts)
136
- return node.with_changes(body=new_body)
137
-
138
- def leave_FunctionDef(
139
- self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
140
- ) -> cst.FunctionDef:
141
- """Returns the function definition with docstring removed or body replaced with ellipsis."""
142
- if self.exclude_fn_body:
143
- stmts = cst.Expr(
144
- value=cst.Ellipsis(
145
- lpar=[],
146
- rpar=[],
147
- ),
148
- semicolon=MaybeSentinel.DEFAULT,
149
- )
150
- return updated_node.with_changes(body=stmts)
151
-
152
- return self._remove_first_docstring(updated_node)
153
-
154
- def leave_ClassDef(
155
- self, original_node: cst.ClassDef, updated_node: cst.ClassDef
156
- ) -> cst.ClassDef:
157
- """Returns the class definition with docstring removed or body replaced with pass."""
158
- if self.exclude_fn_body:
159
- pass_stmt = cst.SimpleStatementLine([cst.Pass()])
160
- new_body = updated_node.body.with_changes(body=[pass_stmt])
161
- return updated_node.with_changes(body=new_body)
162
-
163
- return self._remove_first_docstring(updated_node)
164
-
165
-
166
- def _clean_source_code(
167
- fn: Callable[..., Any] | type,
168
- *,
169
- exclude_fn_body: bool = False,
170
- ) -> str:
171
- """Returns the source code of a function or class with docstrings optionally removed."""
172
- source = dedent(inspect.getsource(fn))
173
- docstr_flag = os.getenv("MIRASCOPE_VERSIONING_INCLUDE_DOCSTRINGS", "false").lower()
174
- if docstr_flag in ("1", "true", "yes"):
175
- return source.rstrip()
176
- module = cst.parse_module(source)
177
- module = module.visit(_RemoveVersionDecoratorTransformer())
178
- module = module.visit(_RemoveDocstringTransformer(exclude_fn_body=exclude_fn_body))
179
- return module.code.rstrip()
180
-
181
-
182
- @dataclass(frozen=True)
183
- class _AttributePath:
184
- """Represents a parsed attribute access path like 'module.class.method'."""
185
-
186
- components: list[str]
187
- """Ordered list from base to final attribute (e.g., ['module', 'class', 'method'])."""
188
-
189
- @property
190
- def base_name(self) -> str:
191
- """Returns the base module or object name."""
192
- return self.components[0] if self.components else ""
193
-
194
- @property
195
- def last_attribute(self) -> str:
196
- """Returns the last attribute in the chain."""
197
- return self.components[-1] if self.components else ""
198
-
199
- @property
200
- def full_path(self) -> str:
201
- """Returns the complete dotted path."""
202
- return ".".join(self.components)
203
-
204
- @classmethod
205
- def from_ast_node(cls, node: ast.AST) -> _AttributePath | None:
206
- """Creates an `_AttributePath` from an AST node.
207
-
208
- Args:
209
- node: An AST node (typically ast.Name, ast.Attribute, or ast.Call).
210
-
211
- Returns:
212
- `_AttributePath` with parsed components, or None if parsing fails.
213
- """
214
- components = []
215
- current = node
216
-
217
- while True:
218
- if isinstance(current, ast.Attribute):
219
- components.append(current.attr)
220
- current = current.value
221
- elif isinstance(current, ast.Call):
222
- current = current.func
223
- elif isinstance(current, ast.Name):
224
- components.append(current.id)
225
- break
226
- else:
227
- break
228
-
229
- components.reverse()
230
- return cls(components=components)
231
-
232
- def __bool__(self) -> bool:
233
- """Returns True if components exist."""
234
- return bool(self.components)
235
-
236
-
237
- class _NameCollector(ast.NodeVisitor):
238
- """AST visitor that collects all names used in a piece of code."""
239
-
240
- def __init__(self) -> None:
241
- self.used_names: list[str] = []
242
-
243
- def visit_Name(self, node: ast.Name) -> None:
244
- """Collects name nodes."""
245
- self.used_names.append(node.id)
246
-
247
- def visit_Call(self, node: ast.Call) -> None:
248
- """Collects function names from call nodes."""
249
- if isinstance(node.func, ast.Name):
250
- self.used_names.append(node.func.id)
251
- self.generic_visit(node)
252
-
253
- def visit_Attribute(self, node: ast.Attribute) -> None:
254
- """Collects attribute access chains."""
255
- attr_path = _AttributePath.from_ast_node(node)
256
- if attr_path:
257
- self.used_names.append(attr_path.full_path)
258
- self.used_names.append(attr_path.base_name)
259
-
260
-
261
- class _ImportCollector(ast.NodeVisitor):
262
- """AST visitor that collects import statements based on used names."""
263
-
264
- def __init__(self, used_names: list[str], site_packages: set[str]) -> None:
265
- self.imports: set[str] = set()
266
- self.user_defined_imports: set[str] = set()
267
- self.used_names = used_names
268
- self.site_packages = site_packages
269
- self.alias_map: dict[str, str] = {}
270
-
271
- def _is_used_import(self, import_name: str) -> bool:
272
- """Returns whether an import with the given name is used in the code."""
273
- return import_name in self.used_names or any(
274
- u.startswith(f"{import_name}.") for u in self.used_names
275
- )
276
-
277
- def _add_import(
278
- self, import_statement: str, is_third_party: bool, alias: str | None = None
279
- ) -> None:
280
- """Adds import statement to appropriate collection."""
281
- if alias:
282
- self.alias_map[alias] = import_statement
283
-
284
- if is_third_party:
285
- self.imports.add(import_statement)
286
- else:
287
- self.user_defined_imports.add(import_statement)
288
-
289
- def visit_Import(self, node: ast.Import) -> None:
290
- """Collects import statements."""
291
- for name in node.names:
292
- full_module_name = name.name
293
- base_module_name = name.name.split(".")[0]
294
- try:
295
- module = __import__(base_module_name)
296
- except ImportError:
297
- module = None
298
- import_name = name.asname or base_module_name
299
-
300
- if not self._is_used_import(import_name):
301
- continue
302
-
303
- is_third_party = (
304
- _is_third_party(module, self.site_packages) if module else False
305
- )
306
-
307
- if alias := name.asname:
308
- import_statement = f"import {full_module_name} as {alias}"
309
- else:
310
- import_statement = f"import {full_module_name}"
311
-
312
- self._add_import(import_statement, is_third_party, name.asname)
313
-
314
- def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
315
- """Collects from-import statements."""
316
- if not (module := node.module):
317
- return
318
-
319
- try:
320
- is_third_party = _is_third_party(
321
- __import__(module.split(".")[0]), self.site_packages
322
- )
323
- except ImportError:
324
- module = "." * node.level + module
325
- is_third_party = False
326
-
327
- for name in node.names:
328
- import_name = name.asname or name.name
329
-
330
- if not self._is_used_import(import_name):
331
- continue
332
-
333
- if alias := name.asname:
334
- import_statement = f"from {module} import {name.name} as {alias}"
335
- else:
336
- import_statement = f"from {module} import {name.name}"
337
-
338
- self._add_import(import_statement, is_third_party, name.asname)
339
-
340
-
341
- class _LocalAssignmentCollector(ast.NodeVisitor):
342
- """AST visitor that collects local variable assignments."""
343
-
344
- def __init__(self) -> None:
345
- self.assignments: set[str] = set()
346
-
347
- def visit_Assign(self, node: ast.Assign) -> None:
348
- """Collects variable names from assignment statements."""
349
- if isinstance(node.targets[0], ast.Name):
350
- self.assignments.add(node.targets[0].id)
351
- self.generic_visit(node)
352
-
353
- def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
354
- """Collects variable names from annotated assignment statements."""
355
- if isinstance(node.target, ast.Name):
356
- self.assignments.add(node.target.id)
357
- self.generic_visit(node)
358
-
359
-
360
- class _GlobalAssignmentCollector(ast.NodeVisitor):
361
- """AST visitor that collects global assignments used in function."""
362
-
363
- def __init__(self, used_names: list[str], source: str) -> None:
364
- self.used_names = used_names
365
- self.source = source
366
- self.assignments: list[str] = []
367
- self.current_function = None
368
- self.current_class = None
369
-
370
- def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
371
- """Tracks function scope while visiting."""
372
- old_function = self.current_function
373
- self.current_function = node
374
- self.generic_visit(node)
375
- self.current_function = old_function
376
-
377
- def visit_ClassDef(self, node: ast.ClassDef) -> None:
378
- """Tracks class scope while visiting."""
379
- old_class = self.current_class
380
- self.current_class = node
381
- self.generic_visit(node)
382
- self.current_class = old_class
383
-
384
- def visit_Assign(self, node: ast.Assign) -> None:
385
- """Collects global assignment statements."""
386
- if self.current_function is not None or self.current_class is not None:
387
- return
388
- for target in node.targets:
389
- if isinstance(target, ast.Name) and target.id in self.used_names:
390
- code = ast.get_source_segment(self.source, node)
391
- if code is not None:
392
- self.assignments.append(code)
393
-
394
- def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
395
- """Collects global annotated assignment statements."""
396
- if self.current_function is not None or self.current_class is not None:
397
- return
398
- if isinstance(node.target, ast.Name) and node.target.id in self.used_names:
399
- code = ast.get_source_segment(self.source, node)
400
- if code is not None:
401
- self.assignments.append(code)
402
-
403
-
404
- def _collect_parameter_names(tree: ast.Module) -> set[str]:
405
- """Returns set of all parameter names from functions in the AST."""
406
- params = set()
407
- for node in ast.walk(tree):
408
- if isinstance(node, ast.FunctionDef):
409
- for arg in node.args.args:
410
- params.add(arg.arg)
411
- for arg in node.args.kwonlyargs:
412
- params.add(arg.arg)
413
- if node.args.vararg:
414
- params.add(node.args.vararg.arg)
415
- if node.args.kwarg:
416
- params.add(node.args.kwarg.arg)
417
- return params
418
-
419
-
420
- def _extract_types(annotation: Any) -> set[type]: # noqa: ANN401
421
- """Returns set of types found in a type annotation."""
422
- types_found: set[type] = set()
423
- origin = get_origin(annotation)
424
-
425
- if origin is not None:
426
- if origin is Annotated:
427
- base_annotation, *_ = get_args(annotation)
428
- types_found |= _extract_types(base_annotation)
429
- else:
430
- for arg in get_args(annotation):
431
- types_found |= _extract_types(arg)
432
- elif isinstance(annotation, type) and not _is_stdlib_or_builtin(annotation):
433
- types_found.add(annotation)
434
- return types_found
435
-
436
-
437
- def _is_stdlib_or_builtin(obj: Any) -> bool: # noqa: ANN401
438
- """Returns True if object is from standard library or builtins."""
439
- if not hasattr(obj, "__module__"):
440
- return False
441
-
442
- module_name = obj.__module__
443
- if not module_name:
444
- return False
445
-
446
- return (
447
- module_name in sys.stdlib_module_names
448
- or module_name.startswith("collections.")
449
- or module_name.startswith("typing.")
450
- or module_name in {"abc", "typing", "builtins", "_collections_abc"}
451
- )
452
-
453
-
454
- class _DefinitionCollector(ast.NodeVisitor):
455
- """AST visitor that collects function and class definitions referenced in code."""
456
-
457
- def __init__(
458
- self, module: ModuleType, used_names: list[str], site_packages: set[str]
459
- ) -> None:
460
- self.module = module
461
- self.used_names = used_names
462
- self.site_packages = site_packages
463
- self.definitions_to_include: list[Callable[..., Any] | type] = []
464
- self.definitions_to_analyze: list[Callable[..., Any] | type] = []
465
- self.imports: set[str] = set()
466
-
467
- def visit_Name(self, node: ast.Name) -> None:
468
- """Collects named references to callable definitions."""
469
- if node.id in self.used_names:
470
- candidate = getattr(self.module, node.id, None)
471
- if callable(candidate) and not _is_stdlib_or_builtin(candidate):
472
- self.definitions_to_include.append(candidate)
473
- self.generic_visit(node)
474
-
475
- def _process_decorator(self, decorator_node: ast.AST) -> None:
476
- """Processes a decorator node to extract its definition."""
477
- if isinstance(decorator_node, ast.Name):
478
- if decorator_func := getattr(self.module, decorator_node.id, None):
479
- self.definitions_to_include.append(decorator_func)
480
- elif isinstance(decorator_node, ast.Attribute):
481
- attr_path = _AttributePath.from_ast_node(decorator_node)
482
- if attr_path:
483
- base_module = getattr(self.module, attr_path.base_name, None)
484
- if (
485
- attr_path.full_path in self.used_names
486
- and base_module
487
- and (
488
- definition := getattr(
489
- base_module, attr_path.last_attribute, None
490
- )
491
- )
492
- ):
493
- self.definitions_to_include.append(definition)
494
-
495
- def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
496
- """Collects function definitions and their decorators."""
497
- for decorator_node in node.decorator_list:
498
- self._process_decorator(decorator_node)
499
-
500
- nested_func = getattr(self.module, node.name, None)
501
- if nested_func:
502
- self.definitions_to_analyze.append(nested_func)
503
-
504
- self.generic_visit(node)
505
-
506
- def visit_ClassDef(self, node: ast.ClassDef) -> None:
507
- """Collects class definitions and their type annotations."""
508
- if class_def := getattr(self.module, node.name, None):
509
- self.definitions_to_analyze.append(class_def)
510
- if hasattr(class_def, "__annotations__"):
511
- for ann in class_def.__annotations__.values():
512
- for candidate in _extract_types(ann):
513
- if (
514
- isinstance(candidate, type)
515
- and candidate.__module__ == class_def.__module__
516
- and candidate.__module__ != "builtins"
517
- ) and candidate not in self.definitions_to_include:
518
- self.definitions_to_include.append(candidate)
519
- for item in node.body:
520
- if isinstance(item, ast.FunctionDef) and (
521
- definition := getattr(class_def, item.name, None)
522
- ):
523
- self.definitions_to_analyze.append(definition)
524
- self.generic_visit(node)
525
-
526
- def _process_name_or_attribute(self, node: ast.AST) -> None:
527
- """Processes name or attribute nodes to find definitions."""
528
- if isinstance(node, ast.Name):
529
- if (
530
- (obj := getattr(self.module, node.id, None))
531
- and hasattr(obj, "__name__")
532
- and not _is_stdlib_or_builtin(obj)
533
- ):
534
- self.definitions_to_include.append(obj)
535
- elif isinstance(node, ast.Attribute):
536
- attr_path = _AttributePath.from_ast_node(node)
537
- if not attr_path or attr_path.full_path not in self.used_names:
538
- return
539
-
540
- base_module = getattr(self.module, attr_path.base_name, None)
541
- if (
542
- base_module
543
- and isinstance(base_module, ModuleType)
544
- and _is_third_party(base_module, self.site_packages)
545
- ):
546
- return
547
-
548
- obj = self.module
549
- for component in attr_path.components:
550
- obj = getattr(obj, component, None)
551
- if obj is None:
552
- break
553
-
554
- if (
555
- obj
556
- and hasattr(obj, "__name__")
557
- and not _is_stdlib_or_builtin(obj)
558
- and not isinstance(obj, ModuleType)
559
- ):
560
- self.definitions_to_include.append(obj)
561
-
562
- def visit_Call(self, node: ast.Call) -> None:
563
- """Collects definitions referenced in function calls."""
564
- self._process_name_or_attribute(node.func)
565
- for arg in node.args:
566
- self._process_name_or_attribute(arg)
567
- for keyword in node.keywords:
568
- self._process_name_or_attribute(keyword.value)
569
- self.generic_visit(node)
570
-
571
-
572
- class _QualifiedNameRewriter(cst.CSTTransformer):
573
- """CST transformer that rewrites qualified names to simple names for local definitions."""
574
-
575
- def __init__(self, local_names: set[str], user_defined_imports: set[str]) -> None:
576
- super().__init__()
577
- self.local_names: set[str] = local_names
578
- self.alias_mapping = {}
579
- for import_stmt in user_defined_imports:
580
- if not import_stmt.startswith("from "):
581
- continue
582
- parts = import_stmt.split(" ")
583
- if len(parts) >= 4 and "as" in parts:
584
- original_name = parts[parts.index("import") + 1]
585
- alias = parts[parts.index("as") + 1]
586
- self.alias_mapping[alias] = original_name
587
-
588
- def _gather_attribute_chain(self, node: cst.Attribute | cst.Name) -> list[str]:
589
- """Returns the chain of attribute names from an attribute node."""
590
- names = []
591
- current = node
592
-
593
- while isinstance(current, cst.Attribute):
594
- names.append(current.attr.value)
595
- current = current.value
596
-
597
- if isinstance(current, cst.Name):
598
- names.append(current.value)
599
-
600
- return list(reversed(names))
601
-
602
- def leave_Attribute(
603
- self, original_node: cst.Attribute, updated_node: cst.Attribute
604
- ) -> cst.Name | cst.Attribute:
605
- """Returns simplified name if attribute refers to local definition."""
606
- names = self._gather_attribute_chain(updated_node)
607
- if names and names[-1] in self.local_names:
608
- return cst.Name(value=names[-1])
609
-
610
- return updated_node
611
-
612
- def leave_Name(self, original_node: cst.Name, updated_node: cst.Name) -> cst.Name:
613
- """Returns de-aliased name if it was imported with an alias."""
614
- if updated_node.value in self.alias_mapping:
615
- return cst.Name(
616
- value=self.alias_mapping[updated_node.value],
617
- lpar=updated_node.lpar,
618
- rpar=updated_node.rpar,
619
- )
620
- return updated_node
621
-
622
-
623
- def _get_class_from_unbound_method(method: Callable[..., Any]) -> type | None:
624
- """Returns the class that contains the given unbound method."""
625
- qualname = method.__qualname__
626
- parts = qualname.split(".")
627
- class_qualname = ".".join(parts[:-1])
628
-
629
- for obj in gc.get_objects():
630
- try:
631
- object_is_type = isinstance(obj, type)
632
- except Exception:
633
- continue
634
- if object_is_type and getattr(obj, "__qualname__", None) == class_qualname:
635
- return obj
636
- return None
637
-
638
-
639
- def _clean_source_from_string(source: str, exclude_fn_body: bool = False) -> str:
640
- """Returns cleaned source code string with optional docstring removal."""
641
- source = dedent(source)
642
- module = cst.parse_module(source)
643
- module = module.visit(_RemoveVersionDecoratorTransformer())
644
- module = module.visit(_RemoveDocstringTransformer(exclude_fn_body=exclude_fn_body))
645
- return module.code.rstrip()
646
-
647
-
648
- def _get_class_source_from_method(method: Callable[..., Any]) -> str:
649
- """Get the source code of the class containing the given method.
650
-
651
- Args:
652
- method: The method to get the containing class source from.
653
-
654
- Returns:
655
- The cleaned source code of the containing class.
656
-
657
- Raises:
658
- ValueError: If the class cannot be determined from the method.
659
- """
660
- cls = _get_class_from_unbound_method(method)
661
- if cls is None:
662
- raise ValueError("Cannot determine class from method via gc")
663
- source = inspect.getsource(cls)
664
- return _clean_source_from_string(source)
665
-
666
-
667
- class _DependencyCollector:
668
- """Collects dependencies, imports, and source code for function closure."""
669
-
670
- def __init__(self) -> None:
671
- self.imports: set[str] = set()
672
- self.fn_internal_imports: set[str] = set()
673
- self.user_defined_imports: set[str] = set()
674
- self.assignments: list[str] = []
675
- self.source_code: list[str] = []
676
- self.visited_functions: set[str] = set()
677
- self.site_packages: set[str] = {
678
- str(Path(p).resolve()) for p in site.getsitepackages()
679
- }
680
- self._last_import_collector: _ImportCollector | None = None
681
-
682
- def _collect_assignments_and_imports(
683
- self,
684
- fn_tree: ast.Module,
685
- module_tree: ast.Module,
686
- used_names: list[str],
687
- module_source: str,
688
- ) -> None:
689
- """Collects global assignments and their required imports."""
690
- local_assignment_collector = _LocalAssignmentCollector()
691
- local_assignment_collector.visit(fn_tree)
692
- local_assignments = local_assignment_collector.assignments
693
-
694
- parameter_names = _collect_parameter_names(fn_tree)
695
-
696
- global_assignment_collector = _GlobalAssignmentCollector(
697
- used_names, module_source
698
- )
699
- global_assignment_collector.visit(module_tree)
700
-
701
- for global_assignment in global_assignment_collector.assignments:
702
- tree = ast.parse(global_assignment)
703
- stmt = cast(ast.Assign | ast.AnnAssign, tree.body[0])
704
- if isinstance(stmt, ast.Assign):
705
- var_name = cast(ast.Name, stmt.targets[0]).id
706
- else:
707
- var_name = cast(ast.Name, stmt.target).id
708
-
709
- if var_name in parameter_names:
710
- continue
711
-
712
- if var_name not in used_names or var_name in local_assignments:
713
- continue
714
-
715
- self.assignments.append(global_assignment)
716
-
717
- name_collector = _NameCollector()
718
- name_collector.visit(tree)
719
- import_collector = _ImportCollector(
720
- name_collector.used_names, self.site_packages
721
- )
722
- import_collector.visit(module_tree)
723
- self.imports.update(import_collector.imports)
724
- self.user_defined_imports.update(import_collector.user_defined_imports)
725
-
726
- @staticmethod
727
- def _extract_definition(
728
- definition: Callable[..., Any] | type | property,
729
- ) -> Callable[..., Any] | type | None:
730
- """Returns the actual definition from decorators and properties."""
731
- if isinstance(definition, property):
732
- return definition.fget
733
-
734
- if isinstance(definition, cached_property) or (
735
- hasattr(definition, "func")
736
- and getattr(definition, "__name__", None) is None
737
- ):
738
- # For Python 3.13+
739
- return definition.func # pyright: ignore[reportFunctionMemberAccess] # pragma: no cover
740
-
741
- if (
742
- (wrapped_function := getattr(definition, "fn", None)) is not None
743
- and not hasattr(definition, "__qualname__")
744
- and callable(wrapped_function)
745
- ):
746
- return wrapped_function
747
-
748
- return definition
749
-
750
- def _get_source_code(self, definition: Callable[..., Any] | type) -> str | None:
751
- """Returns the source code for a definition."""
752
- if definition.__qualname__ in self.visited_functions:
753
- return None
754
- self.visited_functions.add(definition.__qualname__)
755
-
756
- if "." in definition.__qualname__ and inspect.getmodule(definition) is not None:
757
- try:
758
- return _get_class_source_from_method(definition)
759
- except ValueError:
760
- return _clean_source_code(definition)
761
-
762
- return _clean_source_code(definition)
763
-
764
- def _process_imports(
765
- self,
766
- module_tree: ast.Module,
767
- used_names: list[str],
768
- source: str,
769
- ) -> None:
770
- """Process and categorize imports."""
771
- import_collector = _ImportCollector(used_names, self.site_packages)
772
- import_collector.visit(module_tree)
773
-
774
- new_imports = {
775
- import_stmt
776
- for import_stmt in import_collector.imports
777
- if import_stmt not in source
778
- }
779
-
780
- self.imports.update(new_imports)
781
- self.fn_internal_imports.update(import_collector.imports - new_imports)
782
- self.user_defined_imports.update(import_collector.user_defined_imports)
783
-
784
- def _process_definitions(
785
- self, fn_tree: ast.Module, module: ModuleType, used_names: list[str]
786
- ) -> None:
787
- """Process nested definitions and dependencies."""
788
- definition_collector = _DefinitionCollector(
789
- module, used_names, self.site_packages
790
- )
791
- definition_collector.visit(fn_tree)
792
-
793
- for definition in definition_collector.definitions_to_include:
794
- self._collect_imports_and_source_code(definition, True)
795
-
796
- for definition in definition_collector.definitions_to_analyze:
797
- self._collect_imports_and_source_code(definition, False)
798
-
799
- def _collect_imports_and_source_code(
800
- self,
801
- definition: Callable[..., Any] | type | property,
802
- include_source: bool,
803
- ) -> None:
804
- """Collects imports and optionally source code for a definition."""
805
- try:
806
- if _is_stdlib_or_builtin(definition) or isinstance(definition, ModuleType):
807
- return
808
-
809
- # property(fget=None) is not reachable via current code paths but kept as guard
810
- if (
811
- isinstance(definition, property) and definition.fget is None
812
- ): # pragma: no cover
813
- return
814
-
815
- extracted_definition = _DependencyCollector._extract_definition(definition)
816
- # Same guard as above; kept for defensive coding
817
- if extracted_definition is None: # pragma: no cover
818
- return
819
-
820
- source = self._get_source_code(extracted_definition)
821
- if source is None:
822
- return
823
-
824
- module = inspect.getmodule(extracted_definition)
825
- if not module or _is_third_party(module, self.site_packages):
826
- return
827
-
828
- module_source = inspect.getsource(module)
829
- module_tree = ast.parse(module_source)
830
- fn_tree = ast.parse(source)
831
-
832
- name_collector = _NameCollector()
833
- name_collector.visit(fn_tree)
834
- used_names = list(dict.fromkeys(name_collector.used_names))
835
-
836
- self._process_imports(module_tree, used_names, source)
837
-
838
- if include_source:
839
- for import_stmt in self.user_defined_imports:
840
- source = source.replace(import_stmt, "")
841
- self.source_code.insert(0, source)
842
-
843
- self._collect_assignments_and_imports(
844
- fn_tree, module_tree, used_names, module_source
845
- )
846
-
847
- self._process_definitions(fn_tree, module, used_names)
848
-
849
- except (OSError, TypeError) as e:
850
- logger.debug(f"Failed to collect imports for {definition}: {e}")
851
-
852
- @staticmethod
853
- def _collect_required_dependencies(imports: set[str]) -> dict[str, dict[str, Any]]:
854
- """Returns package dependencies required by the import statements."""
855
- stdlib_modules = set(sys.stdlib_module_names)
856
- installed_packages = {
857
- dist.name: dist for dist in importlib.metadata.distributions()
858
- }
859
- import_to_dist = importlib.metadata.packages_distributions()
860
-
861
- dependencies = {}
862
- imported_dists = {}
863
- imported_roots = set()
864
-
865
- for import_stmt in imports:
866
- parts = import_stmt.strip().split()
867
- root_module = parts[1].split(".")[0]
868
- if root_module in stdlib_modules:
869
- continue
870
-
871
- imported_roots.add(root_module)
872
-
873
- dist_names = import_to_dist.get(root_module, [root_module])
874
- for dist_name in dist_names:
875
- if dist_name not in installed_packages:
876
- continue
877
-
878
- dist = installed_packages[dist_name]
879
- imported_dists.setdefault(dist_name, dist)
880
- if dist_name not in dependencies:
881
- dependencies[dist_name] = {
882
- "version": dist.version,
883
- "extras": None,
884
- }
885
- break
886
-
887
- if not imported_dists:
888
- return {}
889
-
890
- dist_to_modules = {}
891
- for module_name, dist_names in import_to_dist.items():
892
- for dist_name in dist_names:
893
- dist_to_modules.setdefault(dist_name, set()).add(module_name)
894
-
895
- base_env = cast(dict[str, str], default_environment().copy())
896
- base_env["extra"] = ""
897
- extra_env_cache = {}
898
-
899
- def _env_for_extra(extra: str) -> dict[str, str]:
900
- if extra not in extra_env_cache:
901
- env = cast(dict[str, str], default_environment().copy())
902
- env["extra"] = extra
903
- extra_env_cache[extra] = env
904
- return extra_env_cache[extra]
905
-
906
- base_requirements = {}
907
- extra_requirements = {}
908
-
909
- for dist_name, dist in imported_dists.items():
910
- base_reqs = set()
911
- extras_map = {
912
- extra: set() for extra in dist.metadata.get_all("Provides-Extra", [])
913
- }
914
- requirements = dist.requires or []
915
- for requirement_str in requirements:
916
- req = Requirement(requirement_str)
917
- marker = req.marker
918
- if marker is None or marker.evaluate(base_env):
919
- base_reqs.add(req.name)
920
- continue
921
-
922
- for extra in extras_map:
923
- if marker.evaluate(_env_for_extra(extra)):
924
- extras_map[extra].add(req.name)
925
-
926
- base_requirements[dist_name] = base_reqs
927
- extra_requirements[dist_name] = extras_map
928
-
929
- provided_requirements = set()
930
- for reqs in base_requirements.values():
931
- provided_requirements.update(reqs)
932
- provided_requirements.update(imported_dists.keys())
933
-
934
- for dist_name in sorted(imported_dists):
935
- extras_to_keep = []
936
- apply_usage_gate = not dist_name.startswith("mirascope")
937
- for extra, deps in extra_requirements[dist_name].items():
938
- if not deps:
939
- continue
940
-
941
- if apply_usage_gate and not any(
942
- dist_to_modules.get(dep, set()) & imported_roots for dep in deps
943
- ):
944
- continue
945
-
946
- missing = [dep for dep in deps if dep not in provided_requirements]
947
- if missing:
948
- extras_to_keep.append(extra)
949
- provided_requirements.update(deps)
950
-
951
- dependencies[dist_name]["extras"] = extras_to_keep or None
952
-
953
- return dependencies
954
-
955
- @classmethod
956
- def _map_child_to_parent(
957
- cls,
958
- child_to_parent: dict[ast.AST, ast.AST | None],
959
- node: ast.AST,
960
- parent: ast.AST | None = None,
961
- ) -> None:
962
- """Maps each AST node to its parent node."""
963
- child_to_parent[node] = parent
964
- for _, value in ast.iter_fields(node):
965
- if isinstance(value, list):
966
- for child in value:
967
- if isinstance(child, ast.AST):
968
- cls._map_child_to_parent(child_to_parent, child, node)
969
- elif isinstance(value, ast.AST):
970
- cls._map_child_to_parent(child_to_parent, value, node)
971
-
972
- def _extract_local_names(self, code_blocks: list[str]) -> set[str]:
973
- """Extracts names of locally defined functions and classes."""
974
- local_names = set()
975
-
976
- for code in code_blocks:
977
- tree = ast.parse(code)
978
- child_to_parent = {}
979
- self._map_child_to_parent(child_to_parent, tree)
980
-
981
- for node in ast.walk(tree):
982
- if isinstance(node, ast.FunctionDef | ast.ClassDef):
983
- parent = child_to_parent.get(node)
984
- if isinstance(parent, ast.Module):
985
- local_names.add(node.name)
986
-
987
- return local_names
988
-
989
- @staticmethod
990
- def _rewrite_code_blocks(
991
- code_blocks: list[str], rewriter: _QualifiedNameRewriter
992
- ) -> list[str]:
993
- """Rewrites code blocks with simplified names."""
994
- rewritten = []
995
- for code in code_blocks:
996
- tree = cst.parse_module(code)
997
- new_tree = tree.visit(rewriter)
998
- rewritten.append(new_tree.code)
999
- return rewritten
1000
-
1001
- def collect(
1002
- self, fn: Callable[..., Any]
1003
- ) -> tuple[list[str], list[str], list[str], dict[str, dict[str, Any]]]:
1004
- """Collects all components needed for function closure.
1005
-
1006
- Args:
1007
- fn: The function to collect closure information for.
1008
-
1009
- Returns:
1010
- A tuple containing:
1011
- - List of import statements
1012
- - List of assignment statements
1013
- - List of source code blocks
1014
- - Dictionary of required dependencies
1015
- """
1016
- self._collect_imports_and_source_code(fn, True)
1017
-
1018
- local_names = self._extract_local_names(self.source_code + self.assignments)
1019
- rewriter = _QualifiedNameRewriter(local_names, self.user_defined_imports)
1020
-
1021
- assignments = self._rewrite_code_blocks(self.assignments, rewriter)
1022
- source_code = self._rewrite_code_blocks(self.source_code, rewriter)
1023
-
1024
- required_dependencies = _DependencyCollector._collect_required_dependencies(
1025
- self.imports | self.fn_internal_imports
1026
- )
1027
-
1028
- return (
1029
- list(self.imports),
1030
- list(dict.fromkeys(assignments)),
1031
- source_code,
1032
- required_dependencies,
1033
- )
1034
-
1035
-
1036
- def _run_ruff(code: str) -> str:
1037
- """Returns formatted code using ruff formatter."""
1038
- with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp_file:
1039
- tmp_file.write(code)
1040
- tmp_path = Path(tmp_file.name)
1041
-
1042
- try:
1043
- proc = subprocess.run(
1044
- ["ruff", "check", "--isolated", "--select=I001", "--fix", str(tmp_path)],
1045
- capture_output=True,
1046
- text=True,
1047
- )
1048
-
1049
- if proc.returncode not in (0, 1):
1050
- raise subprocess.CalledProcessError(
1051
- proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr
1052
- )
1053
-
1054
- subprocess.run(
1055
- ["ruff", "format", "--isolated", "--line-length=88", str(tmp_path)],
1056
- check=True,
1057
- capture_output=True,
1058
- text=True,
1059
- )
1060
- processed_code = tmp_path.read_text()
1061
- return processed_code
1062
- finally:
1063
- tmp_path.unlink()
1064
-
1065
-
1066
- def get_qualified_name(fn: Callable[..., Any]) -> str:
1067
- """Return the simplified qualified name of a function.
1068
-
1069
- If the function is defined locally, return the name after '<locals>.';
1070
- otherwise, return the last non-empty part after splitting by '.'.
1071
-
1072
- Args:
1073
- fn: The function to get the qualified name from.
1074
-
1075
- Returns:
1076
- The simplified qualified name of the function.
1077
- """
1078
- qualified_name = fn.__qualname__
1079
- if "<locals>." in qualified_name:
1080
- return qualified_name.split("<locals>.")[-1]
1081
- else:
1082
- parts = [part for part in qualified_name.split(".") if part]
1083
- return parts[-1] if parts else qualified_name
1084
-
1085
-
1086
- class DependencyInfo(TypedDict):
1087
- """Represents the dependency information for a closure."""
1088
-
1089
- version: str
1090
- """The version of the dependency."""
1091
-
1092
- extras: list[str] | None
1093
- """The extras required for the dependency."""
1094
-
1095
-
1096
- @dataclass(frozen=True, kw_only=True)
1097
- class Closure:
1098
- """Represents the closure of a function."""
1099
-
1100
- name: str
1101
- """The name of the function."""
1102
-
1103
- signature: str
1104
- """The signature of the function."""
1105
-
1106
- docstring: str | None
1107
- """The docstring of the function."""
1108
-
1109
- code: str
1110
- """The code of the function."""
1111
-
1112
- hash: str
1113
- """The hash of the closure."""
1114
-
1115
- signature_hash: str
1116
- """The hash of the function signature (determines major version X)."""
1117
-
1118
- dependencies: dict[str, DependencyInfo]
1119
- """The dependencies of the closure."""
1120
-
1121
- @classmethod
1122
- @lru_cache(maxsize=128)
1123
- def from_fn(cls, fn: Callable[..., Any]) -> Closure:
1124
- """Create a closure from a function.
1125
-
1126
- Args:
1127
- fn: The function to analyze
1128
-
1129
- Returns:
1130
- Closure: The closure of the function.
1131
-
1132
- Raises:
1133
- ClosureComputationError: if the closure cannot be computed properly.
1134
- """
1135
- collector = _DependencyCollector()
1136
- imports, assignments, source_code, dependencies = collector.collect(fn)
1137
- code = "{imports}\n\n{assignments}\n\n{source_code}".format(
1138
- imports="\n".join(imports),
1139
- assignments="\n".join(assignments),
1140
- source_code="\n\n".join(source_code),
1141
- )
1142
- qualified_name = get_qualified_name(fn)
1143
- try:
1144
- formatted_code = _run_ruff(code)
1145
- except (subprocess.CalledProcessError, FileNotFoundError, OSError):
1146
- raise ClosureComputationError(qualified_name=qualified_name)
1147
- hash_value = hashlib.sha256(formatted_code.encode("utf-8")).hexdigest()
1148
-
1149
- signature = _run_ruff(_clean_source_code(fn, exclude_fn_body=True)).strip()
1150
- signature_hash = hashlib.sha256(signature.encode("utf-8")).hexdigest()
1151
-
1152
- return cls(
1153
- name=qualified_name,
1154
- docstring=inspect.getdoc(fn),
1155
- signature=signature,
1156
- code=formatted_code,
1157
- hash=hash_value,
1158
- signature_hash=signature_hash,
1159
- dependencies={
1160
- name: DependencyInfo(
1161
- version=dep_info["version"],
1162
- extras=dep_info.get("extras"),
1163
- )
1164
- for name, dep_info in dependencies.items()
1165
- },
1166
- )
1167
-
1168
-
1169
- __all__ = ["Closure", "DependencyInfo"]