xlwings-server 1.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 (313) hide show
  1. xlwings_server/.env.template +145 -0
  2. xlwings_server/__init__.py +12 -0
  3. xlwings_server/_version.py +34 -0
  4. xlwings_server/auth/__init__.py +0 -0
  5. xlwings_server/auth/custom/__init__.py +26 -0
  6. xlwings_server/auth/entraid/__init__.py +131 -0
  7. xlwings_server/auth/entraid/jwks.py +10 -0
  8. xlwings_server/azure_functions_templates/.funcignore +28 -0
  9. xlwings_server/azure_functions_templates/function_app.py +28 -0
  10. xlwings_server/azure_functions_templates/host.json +22 -0
  11. xlwings_server/azure_functions_templates/local.settings.json +8 -0
  12. xlwings_server/build_utils/__init__.py +9 -0
  13. xlwings_server/build_utils/static_file_hasher.py +212 -0
  14. xlwings_server/cli.py +1592 -0
  15. xlwings_server/config.py +228 -0
  16. xlwings_server/custom_functions/__init__.py +8 -0
  17. xlwings_server/custom_functions/examples.py +177 -0
  18. xlwings_server/custom_scripts/__init__.py +8 -0
  19. xlwings_server/custom_scripts/examples.py +94 -0
  20. xlwings_server/databases.py +19 -0
  21. xlwings_server/dependencies.py +126 -0
  22. xlwings_server/docker_templates/.dockerignore +15 -0
  23. xlwings_server/docker_templates/Dockerfile +60 -0
  24. xlwings_server/docker_templates/docker-compose.yaml +32 -0
  25. xlwings_server/hotreload.py +59 -0
  26. xlwings_server/main.py +242 -0
  27. xlwings_server/models/__init__.py +14 -0
  28. xlwings_server/models/user.py +53 -0
  29. xlwings_server/object_handles.py +142 -0
  30. xlwings_server/routers/__init__.py +0 -0
  31. xlwings_server/routers/manifest.py +82 -0
  32. xlwings_server/routers/root.py +16 -0
  33. xlwings_server/routers/socketio.py +69 -0
  34. xlwings_server/routers/taskpane.py +12 -0
  35. xlwings_server/routers/xlwings.py +197 -0
  36. xlwings_server/security_headers.json +53 -0
  37. xlwings_server/serializers/__init__.py +25 -0
  38. xlwings_server/serializers/default_serializer.py +19 -0
  39. xlwings_server/serializers/dictionary_serializer.py +25 -0
  40. xlwings_server/serializers/framework.py +50 -0
  41. xlwings_server/serializers/numpy_serializer.py +26 -0
  42. xlwings_server/serializers/pandas_serializer.py +95 -0
  43. xlwings_server/static/css/core.css +28 -0
  44. xlwings_server/static/css/style.css +0 -0
  45. xlwings_server/static/images/favicon.png +0 -0
  46. xlwings_server/static/images/xlwings-16.png +0 -0
  47. xlwings_server/static/images/xlwings-32.png +0 -0
  48. xlwings_server/static/images/xlwings-64.png +0 -0
  49. xlwings_server/static/images/xlwings-80.png +0 -0
  50. xlwings_server/static/js/auth.js +13 -0
  51. xlwings_server/static/js/config.js +4 -0
  52. xlwings_server/static/js/core/alpinejs-csp-boilerplate.js +11 -0
  53. xlwings_server/static/js/core/bootstrap-customizations.js +7 -0
  54. xlwings_server/static/js/core/custom-functions-code.js +296 -0
  55. xlwings_server/static/js/core/examples.js +62 -0
  56. xlwings_server/static/js/core/hotreload.js +3 -0
  57. xlwings_server/static/js/core/htmx-handlers.js +86 -0
  58. xlwings_server/static/js/core/officejs-history-fix-part1.js +3 -0
  59. xlwings_server/static/js/core/officejs-history-fix-part2.js +2 -0
  60. xlwings_server/static/js/core/reload-custom-functions.js +79 -0
  61. xlwings_server/static/js/core/socketio-handlers.js +34 -0
  62. xlwings_server/static/js/core/xlwings-alert.js +22 -0
  63. xlwings_server/static/js/core/xlwingsjs/alert.js +85 -0
  64. xlwings_server/static/js/core/xlwingsjs/auth.js +63 -0
  65. xlwings_server/static/js/core/xlwingsjs/sheet-buttons.js +133 -0
  66. xlwings_server/static/js/core/xlwingsjs/utils.js +119 -0
  67. xlwings_server/static/js/core/xlwingsjs/wasm.js +131 -0
  68. xlwings_server/static/js/core/xlwingsjs/xlwings.js +1060 -0
  69. xlwings_server/static/js/main.js +0 -0
  70. xlwings_server/static/js/ribbon.js +17 -0
  71. xlwings_server/static/vendor/@alpinejs/LICENSE +21 -0
  72. xlwings_server/static/vendor/@alpinejs/csp/dist/cdn.min.js +7 -0
  73. xlwings_server/static/vendor/@microsoft/office-js/LICENSE.md +76 -0
  74. xlwings_server/static/vendor/@microsoft/office-js/dist/af-za/office_strings.js +8 -0
  75. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/agaveerrorux.js +18 -0
  76. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/agavedefaulticon32x32.png +0 -0
  77. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/agavedefaulticon96x96.png +0 -0
  78. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/businessbarclose_16x16x32.png +0 -0
  79. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/dropdownarrow_16x16x32.png +0 -0
  80. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/ellipsis_16x16x32.png +0 -0
  81. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/miniinfoblue_16x16x32.png +0 -0
  82. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/moe_default_icon.png +0 -0
  83. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/moe_status_icons.png +0 -0
  84. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/office.png +0 -0
  85. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/refresh_16x16x32.png +0 -0
  86. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/index.html +16 -0
  87. xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/style/agaveerrorux.css +482 -0
  88. xlwings_server/static/vendor/@microsoft/office-js/dist/am-et/office_strings.js +1 -0
  89. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ae/office_strings.js +8 -0
  90. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-bh/office_strings.js +8 -0
  91. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-dz/office_strings.js +8 -0
  92. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-eg/office_strings.js +8 -0
  93. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-iq/office_strings.js +8 -0
  94. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-jo/office_strings.js +8 -0
  95. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-kw/office_strings.js +8 -0
  96. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-lb/office_strings.js +8 -0
  97. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ly/office_strings.js +8 -0
  98. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ma/office_strings.js +8 -0
  99. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-om/office_strings.js +8 -0
  100. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-qa/office_strings.js +8 -0
  101. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-sa/office_strings.js +1 -0
  102. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-sy/office_strings.js +8 -0
  103. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-tn/office_strings.js +8 -0
  104. xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ye/office_strings.js +8 -0
  105. xlwings_server/static/vendor/@microsoft/office-js/dist/ariatelemetry/aria-web-telemetry-2.8.0.min.js +2 -0
  106. xlwings_server/static/vendor/@microsoft/office-js/dist/ariatelemetry/aria-web-telemetry-2.9.0.min.js +2 -0
  107. xlwings_server/static/vendor/@microsoft/office-js/dist/ariatelemetry/aria-web-telemetry.js +1 -0
  108. xlwings_server/static/vendor/@microsoft/office-js/dist/az-latn-az/office_strings.js +8 -0
  109. xlwings_server/static/vendor/@microsoft/office-js/dist/be-by/office_strings.js +8 -0
  110. xlwings_server/static/vendor/@microsoft/office-js/dist/bg-bg/office_strings.js +1 -0
  111. xlwings_server/static/vendor/@microsoft/office-js/dist/bn-in/office_strings.js +1 -0
  112. xlwings_server/static/vendor/@microsoft/office-js/dist/bs-latn-ba/office_strings.js +8 -0
  113. xlwings_server/static/vendor/@microsoft/office-js/dist/ca-es/office_strings.js +1 -0
  114. xlwings_server/static/vendor/@microsoft/office-js/dist/cs-cz/office_strings.js +1 -0
  115. xlwings_server/static/vendor/@microsoft/office-js/dist/cy-gb/office_strings.js +1 -0
  116. xlwings_server/static/vendor/@microsoft/office-js/dist/da-dk/office_strings.js +1 -0
  117. xlwings_server/static/vendor/@microsoft/office-js/dist/de-at/office_strings.js +8 -0
  118. xlwings_server/static/vendor/@microsoft/office-js/dist/de-ch/office_strings.js +8 -0
  119. xlwings_server/static/vendor/@microsoft/office-js/dist/de-de/office_strings.js +1 -0
  120. xlwings_server/static/vendor/@microsoft/office-js/dist/de-li/office_strings.js +8 -0
  121. xlwings_server/static/vendor/@microsoft/office-js/dist/de-lu/office_strings.js +8 -0
  122. xlwings_server/static/vendor/@microsoft/office-js/dist/el-gr/office_strings.js +1 -0
  123. xlwings_server/static/vendor/@microsoft/office-js/dist/en-029/office_strings.js +8 -0
  124. xlwings_server/static/vendor/@microsoft/office-js/dist/en-au/office_strings.js +8 -0
  125. xlwings_server/static/vendor/@microsoft/office-js/dist/en-bz/office_strings.js +8 -0
  126. xlwings_server/static/vendor/@microsoft/office-js/dist/en-ca/office_strings.js +8 -0
  127. xlwings_server/static/vendor/@microsoft/office-js/dist/en-gb/office_strings.js +8 -0
  128. xlwings_server/static/vendor/@microsoft/office-js/dist/en-ie/office_strings.js +8 -0
  129. xlwings_server/static/vendor/@microsoft/office-js/dist/en-in/office_strings.js +8 -0
  130. xlwings_server/static/vendor/@microsoft/office-js/dist/en-jm/office_strings.js +8 -0
  131. xlwings_server/static/vendor/@microsoft/office-js/dist/en-my/office_strings.js +8 -0
  132. xlwings_server/static/vendor/@microsoft/office-js/dist/en-nz/office_strings.js +8 -0
  133. xlwings_server/static/vendor/@microsoft/office-js/dist/en-ph/office_strings.js +8 -0
  134. xlwings_server/static/vendor/@microsoft/office-js/dist/en-sg/office_strings.js +8 -0
  135. xlwings_server/static/vendor/@microsoft/office-js/dist/en-tt/office_strings.js +8 -0
  136. xlwings_server/static/vendor/@microsoft/office-js/dist/en-us/office_strings.js +8 -0
  137. xlwings_server/static/vendor/@microsoft/office-js/dist/en-za/office_strings.js +8 -0
  138. xlwings_server/static/vendor/@microsoft/office-js/dist/en-zw/office_strings.js +8 -0
  139. xlwings_server/static/vendor/@microsoft/office-js/dist/es-ar/office_strings.js +8 -0
  140. xlwings_server/static/vendor/@microsoft/office-js/dist/es-bo/office_strings.js +8 -0
  141. xlwings_server/static/vendor/@microsoft/office-js/dist/es-cl/office_strings.js +8 -0
  142. xlwings_server/static/vendor/@microsoft/office-js/dist/es-co/office_strings.js +8 -0
  143. xlwings_server/static/vendor/@microsoft/office-js/dist/es-cr/office_strings.js +8 -0
  144. xlwings_server/static/vendor/@microsoft/office-js/dist/es-do/office_strings.js +8 -0
  145. xlwings_server/static/vendor/@microsoft/office-js/dist/es-ec/office_strings.js +8 -0
  146. xlwings_server/static/vendor/@microsoft/office-js/dist/es-es/office_strings.js +1 -0
  147. xlwings_server/static/vendor/@microsoft/office-js/dist/es-gt/office_strings.js +8 -0
  148. xlwings_server/static/vendor/@microsoft/office-js/dist/es-hn/office_strings.js +8 -0
  149. xlwings_server/static/vendor/@microsoft/office-js/dist/es-mx/office_strings.js +1 -0
  150. xlwings_server/static/vendor/@microsoft/office-js/dist/es-ni/office_strings.js +8 -0
  151. xlwings_server/static/vendor/@microsoft/office-js/dist/es-pa/office_strings.js +8 -0
  152. xlwings_server/static/vendor/@microsoft/office-js/dist/es-pe/office_strings.js +8 -0
  153. xlwings_server/static/vendor/@microsoft/office-js/dist/es-pr/office_strings.js +8 -0
  154. xlwings_server/static/vendor/@microsoft/office-js/dist/es-py/office_strings.js +8 -0
  155. xlwings_server/static/vendor/@microsoft/office-js/dist/es-sv/office_strings.js +8 -0
  156. xlwings_server/static/vendor/@microsoft/office-js/dist/es-us/office_strings.js +8 -0
  157. xlwings_server/static/vendor/@microsoft/office-js/dist/es-uy/office_strings.js +8 -0
  158. xlwings_server/static/vendor/@microsoft/office-js/dist/es-ve/office_strings.js +8 -0
  159. xlwings_server/static/vendor/@microsoft/office-js/dist/es6-promise.js +5 -0
  160. xlwings_server/static/vendor/@microsoft/office-js/dist/et-ee/office_strings.js +1 -0
  161. xlwings_server/static/vendor/@microsoft/office-js/dist/eu-es/office_strings.js +1 -0
  162. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-15.01.js +11 -0
  163. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-15.02.js +11 -0
  164. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-15.js +11 -0
  165. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-mac-16.00-core.js +11 -0
  166. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-mac-16.00.js +25 -0
  167. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-web-16.00-core.js +11 -0
  168. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-web-16.00.js +25 -0
  169. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-win32-16.00.js +19 -0
  170. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-win32-16.01-core.js +11 -0
  171. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-win32-16.01.js +25 -0
  172. xlwings_server/static/vendor/@microsoft/office-js/dist/excel-winrt-16.00.js +25 -0
  173. xlwings_server/static/vendor/@microsoft/office-js/dist/excelios-15.js +11 -0
  174. xlwings_server/static/vendor/@microsoft/office-js/dist/excelwebapp-15.01.js +11 -0
  175. xlwings_server/static/vendor/@microsoft/office-js/dist/excelwebapp-15.02.js +11 -0
  176. xlwings_server/static/vendor/@microsoft/office-js/dist/excelwebapp-15.js +11 -0
  177. xlwings_server/static/vendor/@microsoft/office-js/dist/fa-ir/office_strings.js +1 -0
  178. xlwings_server/static/vendor/@microsoft/office-js/dist/fi-fi/office_strings.js +1 -0
  179. xlwings_server/static/vendor/@microsoft/office-js/dist/fil-ph/office_strings.js +1 -0
  180. xlwings_server/static/vendor/@microsoft/office-js/dist/fr-be/office_strings.js +8 -0
  181. xlwings_server/static/vendor/@microsoft/office-js/dist/fr-ca/office_strings.js +1 -0
  182. xlwings_server/static/vendor/@microsoft/office-js/dist/fr-ch/office_strings.js +8 -0
  183. xlwings_server/static/vendor/@microsoft/office-js/dist/fr-fr/office_strings.js +1 -0
  184. xlwings_server/static/vendor/@microsoft/office-js/dist/fr-lu/office_strings.js +8 -0
  185. xlwings_server/static/vendor/@microsoft/office-js/dist/fr-mc/office_strings.js +8 -0
  186. xlwings_server/static/vendor/@microsoft/office-js/dist/ga-ie/office_strings.js +8 -0
  187. xlwings_server/static/vendor/@microsoft/office-js/dist/gl-es/office_strings.js +1 -0
  188. xlwings_server/static/vendor/@microsoft/office-js/dist/gu-in/office_strings.js +1 -0
  189. xlwings_server/static/vendor/@microsoft/office-js/dist/he-il/office_strings.js +1 -0
  190. xlwings_server/static/vendor/@microsoft/office-js/dist/hi-in/office_strings.js +1 -0
  191. xlwings_server/static/vendor/@microsoft/office-js/dist/hr-ba/office_strings.js +8 -0
  192. xlwings_server/static/vendor/@microsoft/office-js/dist/hr-hr/office_strings.js +1 -0
  193. xlwings_server/static/vendor/@microsoft/office-js/dist/html2canvas.js +8 -0
  194. xlwings_server/static/vendor/@microsoft/office-js/dist/hu-hu/office_strings.js +1 -0
  195. xlwings_server/static/vendor/@microsoft/office-js/dist/hy-am/office_strings.js +8 -0
  196. xlwings_server/static/vendor/@microsoft/office-js/dist/id-id/office_strings.js +1 -0
  197. xlwings_server/static/vendor/@microsoft/office-js/dist/is-is/office_strings.js +1 -0
  198. xlwings_server/static/vendor/@microsoft/office-js/dist/it-ch/office_strings.js +8 -0
  199. xlwings_server/static/vendor/@microsoft/office-js/dist/it-it/office_strings.js +1 -0
  200. xlwings_server/static/vendor/@microsoft/office-js/dist/ja-jp/office_strings.js +1 -0
  201. xlwings_server/static/vendor/@microsoft/office-js/dist/ka-ge/office_strings.js +8 -0
  202. xlwings_server/static/vendor/@microsoft/office-js/dist/kk-kz/office_strings.js +1 -0
  203. xlwings_server/static/vendor/@microsoft/office-js/dist/km-kh/office_strings.js +8 -0
  204. xlwings_server/static/vendor/@microsoft/office-js/dist/kn-in/office_strings.js +1 -0
  205. xlwings_server/static/vendor/@microsoft/office-js/dist/ko-kr/office_strings.js +1 -0
  206. xlwings_server/static/vendor/@microsoft/office-js/dist/lb-lu/office_strings.js +8 -0
  207. xlwings_server/static/vendor/@microsoft/office-js/dist/lo-la/office_strings.js +1 -0
  208. xlwings_server/static/vendor/@microsoft/office-js/dist/lt-lt/office_strings.js +1 -0
  209. xlwings_server/static/vendor/@microsoft/office-js/dist/lv-lv/office_strings.js +1 -0
  210. xlwings_server/static/vendor/@microsoft/office-js/dist/mk-mk/office_strings.js +8 -0
  211. xlwings_server/static/vendor/@microsoft/office-js/dist/ml-in/office_strings.js +1 -0
  212. xlwings_server/static/vendor/@microsoft/office-js/dist/mn-mn/office_strings.js +8 -0
  213. xlwings_server/static/vendor/@microsoft/office-js/dist/mr-in/office_strings.js +1 -0
  214. xlwings_server/static/vendor/@microsoft/office-js/dist/ms-bn/office_strings.js +8 -0
  215. xlwings_server/static/vendor/@microsoft/office-js/dist/ms-my/office_strings.js +1 -0
  216. xlwings_server/static/vendor/@microsoft/office-js/dist/mt-mt/office_strings.js +8 -0
  217. xlwings_server/static/vendor/@microsoft/office-js/dist/nb-no/office_strings.js +1 -0
  218. xlwings_server/static/vendor/@microsoft/office-js/dist/ne-np/office_strings.js +8 -0
  219. xlwings_server/static/vendor/@microsoft/office-js/dist/nl-be/office_strings.js +8 -0
  220. xlwings_server/static/vendor/@microsoft/office-js/dist/nl-nl/office_strings.js +1 -0
  221. xlwings_server/static/vendor/@microsoft/office-js/dist/nn-no/office_strings.js +1 -0
  222. xlwings_server/static/vendor/@microsoft/office-js/dist/o15apptofilemappingtable.js +11 -0
  223. xlwings_server/static/vendor/@microsoft/office-js/dist/office-vsdoc.js +28596 -0
  224. xlwings_server/static/vendor/@microsoft/office-js/dist/office.js +84 -0
  225. xlwings_server/static/vendor/@microsoft/office-js/dist/pl-pl/office_strings.js +1 -0
  226. xlwings_server/static/vendor/@microsoft/office-js/dist/pt-br/office_strings.js +1 -0
  227. xlwings_server/static/vendor/@microsoft/office-js/dist/pt-pt/office_strings.js +1 -0
  228. xlwings_server/static/vendor/@microsoft/office-js/dist/ro-ro/office_strings.js +1 -0
  229. xlwings_server/static/vendor/@microsoft/office-js/dist/ru-ru/office_strings.js +1 -0
  230. xlwings_server/static/vendor/@microsoft/office-js/dist/si-lk/office_strings.js +8 -0
  231. xlwings_server/static/vendor/@microsoft/office-js/dist/sk-sk/office_strings.js +1 -0
  232. xlwings_server/static/vendor/@microsoft/office-js/dist/sl-si/office_strings.js +1 -0
  233. xlwings_server/static/vendor/@microsoft/office-js/dist/sq-al/office_strings.js +8 -0
  234. xlwings_server/static/vendor/@microsoft/office-js/dist/sr-cyrl-cs/office_strings.js +1 -0
  235. xlwings_server/static/vendor/@microsoft/office-js/dist/sr-cyrl-rs/office_strings.js +1 -0
  236. xlwings_server/static/vendor/@microsoft/office-js/dist/sr-latn-cs/office_strings.js +1 -0
  237. xlwings_server/static/vendor/@microsoft/office-js/dist/sr-latn-rs/office_strings.js +1 -0
  238. xlwings_server/static/vendor/@microsoft/office-js/dist/sv-fi/office_strings.js +8 -0
  239. xlwings_server/static/vendor/@microsoft/office-js/dist/sv-se/office_strings.js +1 -0
  240. xlwings_server/static/vendor/@microsoft/office-js/dist/sw-ke/office_strings.js +1 -0
  241. xlwings_server/static/vendor/@microsoft/office-js/dist/ta-in/office_strings.js +1 -0
  242. xlwings_server/static/vendor/@microsoft/office-js/dist/te-in/office_strings.js +1 -0
  243. xlwings_server/static/vendor/@microsoft/office-js/dist/telemetry/oteljs.js +1 -0
  244. xlwings_server/static/vendor/@microsoft/office-js/dist/telemetry/oteljs_agave.js +1 -0
  245. xlwings_server/static/vendor/@microsoft/office-js/dist/th-th/office_strings.js +1 -0
  246. xlwings_server/static/vendor/@microsoft/office-js/dist/tr-tr/office_strings.js +1 -0
  247. xlwings_server/static/vendor/@microsoft/office-js/dist/uk-ua/office_strings.js +1 -0
  248. xlwings_server/static/vendor/@microsoft/office-js/dist/ur-pk/office_strings.js +1 -0
  249. xlwings_server/static/vendor/@microsoft/office-js/dist/vi-vn/office_strings.js +1 -0
  250. xlwings_server/static/vendor/@microsoft/office-js/dist/webauth/webauth.browserauth.js +77 -0
  251. xlwings_server/static/vendor/@microsoft/office-js/dist/webauth/webauth.implicit.js +35 -0
  252. xlwings_server/static/vendor/@microsoft/office-js/dist/zh-cn/office_strings.js +1 -0
  253. xlwings_server/static/vendor/@microsoft/office-js/dist/zh-hk/office_strings.js +8 -0
  254. xlwings_server/static/vendor/@microsoft/office-js/dist/zh-mo/office_strings.js +8 -0
  255. xlwings_server/static/vendor/@microsoft/office-js/dist/zh-sg/office_strings.js +8 -0
  256. xlwings_server/static/vendor/@microsoft/office-js/dist/zh-tw/office_strings.js +1 -0
  257. xlwings_server/static/vendor/axios/dist/axios.min.js +3 -0
  258. xlwings_server/static/vendor/axios/dist/axios.min.js.map +1 -0
  259. xlwings_server/static/vendor/bootstrap/LICENSE +21 -0
  260. xlwings_server/static/vendor/bootstrap/dist/js/bootstrap.bundle.min.js +7 -0
  261. xlwings_server/static/vendor/bootstrap/dist/js/bootstrap.bundle.min.js.map +1 -0
  262. xlwings_server/static/vendor/bootstrap-xlwings/dist/bootstrap-xlwings.min.css +12 -0
  263. xlwings_server/static/vendor/bootstrap-xlwings/dist/bootstrap-xlwings.min.css.map +1 -0
  264. xlwings_server/static/vendor/htmx-ext-head-support/head-support.js +144 -0
  265. xlwings_server/static/vendor/htmx-ext-loading-states/loading-states.js +184 -0
  266. xlwings_server/static/vendor/htmx.org/LICENSE +13 -0
  267. xlwings_server/static/vendor/htmx.org/dist/htmx.min.js +1 -0
  268. xlwings_server/static/vendor/socket.io/LICENSE +22 -0
  269. xlwings_server/static/vendor/socket.io/client-dist/socket.io.min.js +7 -0
  270. xlwings_server/static/vendor/socket.io/client-dist/socket.io.min.js.map +1 -0
  271. xlwings_server/templates/_book.html +8 -0
  272. xlwings_server/templates/alert_base.html +16 -0
  273. xlwings_server/templates/base.html +117 -0
  274. xlwings_server/templates/examples/alpine/README.md +26 -0
  275. xlwings_server/templates/examples/alpine/taskpane.html +47 -0
  276. xlwings_server/templates/examples/auth/README.md +38 -0
  277. xlwings_server/templates/examples/auth/protected.html +8 -0
  278. xlwings_server/templates/examples/auth/public.html +11 -0
  279. xlwings_server/templates/examples/excel_object_model/README.md +49 -0
  280. xlwings_server/templates/examples/excel_object_model/add_name_form.html +27 -0
  281. xlwings_server/templates/examples/hello_world/README.md +9 -0
  282. xlwings_server/templates/examples/hello_world/taskpane_hello.html +24 -0
  283. xlwings_server/templates/examples/htmx_form/README.md +44 -0
  284. xlwings_server/templates/examples/htmx_form/_greeting.html +6 -0
  285. xlwings_server/templates/examples/htmx_form/taskpane_htmx_form.html +21 -0
  286. xlwings_server/templates/examples/live_form_validation/README.md +60 -0
  287. xlwings_server/templates/examples/live_form_validation/add_name_form.html +33 -0
  288. xlwings_server/templates/examples/multi_app/README.md +34 -0
  289. xlwings_server/templates/examples/multi_app/taskpane1.html +7 -0
  290. xlwings_server/templates/examples/multi_app/taskpane2.html +7 -0
  291. xlwings_server/templates/examples/multi_app/taskpane_loader.html +5 -0
  292. xlwings_server/templates/examples/navigation/README.md +28 -0
  293. xlwings_server/templates/examples/navigation/_navigation.html +16 -0
  294. xlwings_server/templates/examples/navigation/taskpane_one.html +8 -0
  295. xlwings_server/templates/examples/navigation/taskpane_three.html +8 -0
  296. xlwings_server/templates/examples/navigation/taskpane_two.html +8 -0
  297. xlwings_server/templates/examples/pictures/README.md +42 -0
  298. xlwings_server/templates/examples/pictures/_picture.html +4 -0
  299. xlwings_server/templates/examples/pictures/taskpane_pictures.html +26 -0
  300. xlwings_server/templates/manifest.xml +155 -0
  301. xlwings_server/templates/taskpane.html +1 -0
  302. xlwings_server/templates/xlwings_alert.html +27 -0
  303. xlwings_server/templates.py +61 -0
  304. xlwings_server/utils.py +32 -0
  305. xlwings_server/wasm/__init__.py +0 -0
  306. xlwings_server/wasm/config.py +24 -0
  307. xlwings_server/wasm/main.py +236 -0
  308. xlwings_server/wasm/requirements.txt +5 -0
  309. xlwings_server-1.1.0.dist-info/METADATA +61 -0
  310. xlwings_server-1.1.0.dist-info/RECORD +313 -0
  311. xlwings_server-1.1.0.dist-info/WHEEL +4 -0
  312. xlwings_server-1.1.0.dist-info/entry_points.txt +2 -0
  313. xlwings_server-1.1.0.dist-info/licenses/LICENSE.md +223 -0
xlwings_server/cli.py ADDED
@@ -0,0 +1,1592 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import sys
6
+ import uuid
7
+ import zipfile
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from textwrap import dedent
11
+ from urllib.parse import urljoin, urlparse
12
+
13
+ import uvicorn
14
+
15
+ from xlwings_server.build_utils import StaticFileHasher
16
+ from xlwings_server.config import PACKAGE_DIR, PROJECT_DIR
17
+
18
+
19
+ # Helper Classes and Functions
20
+ class FileTracker:
21
+ """Track created and skipped files during CLI operations."""
22
+
23
+ def __init__(self):
24
+ self.created = []
25
+ self.skipped = []
26
+
27
+ def mark_created(self, path: str):
28
+ """Mark a file as created."""
29
+ self.created.append(path)
30
+
31
+ def mark_skipped(self, path: str):
32
+ """Mark a file as skipped (already exists)."""
33
+ self.skipped.append(path)
34
+
35
+ def print_summary(self, operation_name: str):
36
+ """Print summary of created and skipped files."""
37
+ print(f"\n{operation_name} complete!")
38
+
39
+
40
+ def validate_project_directory() -> Path:
41
+ """Validate we're in an xlwings-server project directory."""
42
+ project_path = Path.cwd()
43
+ if not (project_path / "custom_functions").exists():
44
+ print("Error: Not in an xlwings-server project directory.")
45
+ print("Run this command from your project root.")
46
+ print("Hint: Initialize a project first with 'xlwings-server init'")
47
+ sys.exit(1)
48
+ return project_path
49
+
50
+
51
+ def copy_file_if_not_exists(
52
+ source: Path,
53
+ dest: Path,
54
+ tracker: FileTracker,
55
+ display_name: str | None = None,
56
+ ) -> bool:
57
+ """Copy file if it doesn't exist. Returns True if copied."""
58
+ display_name = display_name or str(dest)
59
+ if dest.exists():
60
+ tracker.mark_skipped(display_name)
61
+ return False
62
+ else:
63
+ dest.parent.mkdir(parents=True, exist_ok=True)
64
+ shutil.copy(source, dest)
65
+ tracker.mark_created(display_name)
66
+ return True
67
+
68
+
69
+ def create_sample_file_if_not_exists(file_path: Path, content: str) -> bool:
70
+ """Create a sample file if it doesn't exist. Returns True if created."""
71
+ if file_path.exists():
72
+ return False
73
+ file_path.write_text(dedent(content))
74
+ return True
75
+
76
+
77
+ def _ignore_dirs(directory: str, files: list[str]) -> set[str]: # noqa: ARG001
78
+ """Ignore function for shutil.copytree to skip .claude and __pycache__ dirs."""
79
+ return {f for f in files if f in (".claude", "__pycache__")}
80
+
81
+
82
+ def _copy_folder(source_dir: Path, dest_dir: Path, folder_name: str) -> None:
83
+ """Copy folder, replacing destination if it exists."""
84
+ if source_dir.exists():
85
+ if dest_dir.exists():
86
+ shutil.rmtree(dest_dir)
87
+ shutil.copytree(source_dir, dest_dir, ignore=_ignore_dirs)
88
+ print(f"Copied {folder_name}.")
89
+ else:
90
+ print(f"No {folder_name} folder found.")
91
+
92
+
93
+ def _copy_folder_merge(source_dir: Path, dest_dir: Path, folder_name: str) -> None:
94
+ """Copy folder contents, merging with existing files (overwrites on conflict)."""
95
+ if source_dir.exists():
96
+ shutil.copytree(source_dir, dest_dir, dirs_exist_ok=True, ignore=_ignore_dirs)
97
+ print(f"Merged {folder_name}.")
98
+
99
+
100
+ # Migration Helper Functions
101
+ def validate_old_project_directory(old_path: Path) -> Path:
102
+ """Validate that old_path contains a valid pre-1.0 project structure"""
103
+ if not old_path.exists():
104
+ print(f"Error: Path does not exist: {old_path}")
105
+ sys.exit(1)
106
+
107
+ if not old_path.is_dir():
108
+ print(f"Error: Path is not a directory: {old_path}")
109
+ sys.exit(1)
110
+
111
+ # Check for app/ directory
112
+ app_dir = old_path / "app"
113
+ if not app_dir.exists():
114
+ print("Error: Not an old xlwings-server project (no app/ directory found)")
115
+ print(f"Expected to find: {app_dir}")
116
+ sys.exit(1)
117
+
118
+ # Check for app/config.py
119
+ config_file = app_dir / "config.py"
120
+ if not config_file.exists():
121
+ print("Error: Not an old xlwings-server project (no app/config.py found)")
122
+ print(f"Expected to find: {config_file}")
123
+ sys.exit(1)
124
+
125
+ # Check for app/custom_functions and app/custom_scripts
126
+ if not (app_dir / "custom_functions").exists():
127
+ print("Error: app/custom_functions/ directory not found")
128
+ sys.exit(1)
129
+
130
+ if not (app_dir / "custom_scripts").exists():
131
+ print("Error: app/custom_scripts/ directory not found")
132
+ sys.exit(1)
133
+
134
+ return old_path
135
+
136
+
137
+ def extract_uuids_from_old_config(config_path: Path) -> dict[str, str]:
138
+ """Extract manifest UUIDs from app/config.py using regex"""
139
+ import re
140
+
141
+ content = config_path.read_text()
142
+ uuids = {}
143
+
144
+ # Pattern matches both styles:
145
+ # manifest_id_dev = "uuid-here"
146
+ # manifest_id_dev: UUID4 = "uuid-here"
147
+ pattern = r'manifest_id_(\w+)\s*[:=]\s*(?:UUID4\s*=\s*)?["\']([0-9a-f\-]{36})["\']'
148
+
149
+ for match in re.finditer(pattern, content, re.MULTILINE):
150
+ env_name, uuid_val = match.groups()
151
+ uuids[f"manifest_id_{env_name}"] = uuid_val
152
+
153
+ return uuids
154
+
155
+
156
+ def update_uuids_in_pyproject(uuids: dict[str, str]):
157
+ """Update pyproject.toml with extracted UUIDs using tomlkit"""
158
+ import tomlkit
159
+
160
+ pyproject_path = Path("pyproject.toml")
161
+
162
+ if not pyproject_path.exists():
163
+ print("Error: pyproject.toml not found in current directory")
164
+ sys.exit(1)
165
+
166
+ content = tomlkit.parse(pyproject_path.read_text())
167
+
168
+ # Ensure [tool.xlwings_server] section exists
169
+ if "tool" not in content:
170
+ content["tool"] = {}
171
+ if "xlwings_server" not in content["tool"]:
172
+ content["tool"]["xlwings_server"] = {}
173
+
174
+ # Update UUIDs
175
+ for key, value in uuids.items():
176
+ content["tool"]["xlwings_server"][key] = value
177
+
178
+ pyproject_path.write_text(tomlkit.dumps(content))
179
+
180
+
181
+ def is_file_customized(file_path: Path) -> bool:
182
+ """Check if file exists and is not empty"""
183
+ if not file_path.exists():
184
+ return False
185
+
186
+ # Check if file has content (not just whitespace)
187
+ content = file_path.read_text().strip()
188
+ return len(content) > 0
189
+
190
+
191
+ def copy_directory_recursive(
192
+ source: Path,
193
+ dest: Path,
194
+ tracker: FileTracker,
195
+ exclude_patterns: list[str] | None = None,
196
+ ):
197
+ """Recursively copy directory contents, tracking all files"""
198
+ import shutil
199
+
200
+ if not source.exists():
201
+ return
202
+
203
+ exclude_patterns = exclude_patterns or []
204
+
205
+ for item in source.rglob("*"):
206
+ if item.is_file():
207
+ # Check if file matches any exclude pattern
208
+ relative_path = item.relative_to(source)
209
+ should_exclude = any(
210
+ pattern in str(relative_path) for pattern in exclude_patterns
211
+ )
212
+
213
+ if should_exclude:
214
+ continue
215
+
216
+ dest_file = dest / relative_path
217
+
218
+ # Create parent directories
219
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
220
+
221
+ # Copy file
222
+ shutil.copy2(item, dest_file)
223
+ tracker.mark_created(str(dest / relative_path))
224
+
225
+
226
+ # Project Setup Functions
227
+ def create_project_structure(project_path: Path):
228
+ """Create minimal project structure"""
229
+ # Create project directory if it doesn't exist
230
+ project_path.mkdir(parents=True, exist_ok=True)
231
+
232
+ # Create required directories
233
+ (project_path / "custom_functions").mkdir(exist_ok=True)
234
+ (project_path / "custom_scripts").mkdir(exist_ok=True)
235
+ (project_path / "templates").mkdir(exist_ok=True)
236
+ (project_path / "certs").mkdir(exist_ok=True)
237
+
238
+ # Create __init__.py files with imports
239
+ functions_init = project_path / "custom_functions" / "__init__.py"
240
+ if not functions_init.exists():
241
+ functions_init.write_text(
242
+ "# Import your custom functions here\nfrom .functions import *\n"
243
+ )
244
+
245
+ scripts_init = project_path / "custom_scripts" / "__init__.py"
246
+ if not scripts_init.exists():
247
+ scripts_init.write_text(
248
+ "# Import your custom scripts here\nfrom .scripts import *\n"
249
+ )
250
+
251
+ # Create sample files
252
+ create_sample_functions(project_path)
253
+ create_sample_scripts(project_path)
254
+ create_sample_taskpane(project_path)
255
+
256
+
257
+ def create_sample_functions(project_path: Path):
258
+ """Create sample functions.py file with hello function"""
259
+ functions_file = project_path / "custom_functions" / "functions.py"
260
+ sample_code = """\
261
+ from xlwings import func
262
+
263
+
264
+ @func
265
+ def hello(name):
266
+ return f"Hello {name}!"
267
+ """
268
+ create_sample_file_if_not_exists(functions_file, sample_code)
269
+
270
+
271
+ def create_sample_scripts(project_path: Path):
272
+ """Create sample scripts.py file with hello_world script"""
273
+ scripts_file = project_path / "custom_scripts" / "scripts.py"
274
+ sample_code = """\
275
+ import xlwings as xw
276
+ from xlwings import script
277
+
278
+
279
+ @script
280
+ def hello_world(book: xw.Book):
281
+ sheet = book.sheets.active
282
+ cell = sheet["A1"]
283
+ if cell.value == "Hello xlwings!":
284
+ cell.value = "Bye xlwings!"
285
+ else:
286
+ cell.value = "Hello xlwings!"
287
+ """
288
+ create_sample_file_if_not_exists(scripts_file, sample_code)
289
+
290
+
291
+ def create_sample_taskpane(project_path: Path):
292
+ """Create sample taskpane.html template"""
293
+ taskpane_file = project_path / "templates" / "taskpane.html"
294
+ sample_html = """\
295
+ {% extends "base.html" %}
296
+
297
+ {% block content %}
298
+ <div class="container-fluid pt-3 ps-3">
299
+ <h1>{{ settings.project_name }}</h1>
300
+ <button xw-click="hello_world" class="btn btn-primary btn-sm" type="button">
301
+ Write 'Hello/Bye xlwings!' to A1
302
+ </button>
303
+ </div>
304
+ {% endblock content %}
305
+ """
306
+ create_sample_file_if_not_exists(taskpane_file, sample_html)
307
+
308
+
309
+ def add_router_command():
310
+ """Add router directory and sample router to project"""
311
+ project_path = validate_project_directory()
312
+ tracker = FileTracker()
313
+
314
+ # Create routers directory
315
+ routers_dir = project_path / "routers"
316
+ routers_dir.mkdir(exist_ok=True)
317
+
318
+ # Create __init__.py (empty)
319
+ init_file = routers_dir / "__init__.py"
320
+ if not init_file.exists():
321
+ init_file.write_text("")
322
+
323
+ # Create sample custom.py
324
+ sample_file = routers_dir / "custom.py"
325
+ sample_code = """\
326
+ from fastapi import APIRouter, Request
327
+
328
+ from xlwings_server import settings
329
+ from xlwings_server.templates import TemplateResponse
330
+
331
+ router = APIRouter(prefix=settings.app_path)
332
+
333
+
334
+ @router.get("/hello-json")
335
+ async def hello_json():
336
+ return {"message": "Hello from custom router!"}
337
+
338
+
339
+ @router.get("/hello-template")
340
+ async def hello_template(request: Request):
341
+ return TemplateResponse(
342
+ request=request,
343
+ name="examples/hello_world/taskpane_hello.html",
344
+ )
345
+
346
+ """
347
+
348
+ if sample_file.exists():
349
+ tracker.mark_skipped("custom.py")
350
+ else:
351
+ sample_file.write_text(dedent(sample_code))
352
+ tracker.mark_created("custom.py")
353
+
354
+ tracker.print_summary("Router setup")
355
+
356
+
357
+ def add_model_user_command():
358
+ """Add user model to project for customization"""
359
+ project_path = validate_project_directory()
360
+ tracker = FileTracker()
361
+
362
+ # Create models directory
363
+ models_dir = project_path / "models"
364
+ models_dir.mkdir(exist_ok=True)
365
+
366
+ # Create __init__.py
367
+ init_file = models_dir / "__init__.py"
368
+ if not init_file.exists():
369
+ init_file.write_text("from .user import User\n")
370
+
371
+ # Copy user.py from package
372
+ source_file = PACKAGE_DIR / "models" / "user.py"
373
+ dest_file = models_dir / "user.py"
374
+ copy_file_if_not_exists(source_file, dest_file, tracker, "user.py")
375
+
376
+ tracker.print_summary("User model setup")
377
+
378
+
379
+ def add_auth_custom_command():
380
+ """Add custom auth provider to project for customization"""
381
+ import json
382
+
383
+ from dotenv import dotenv_values, set_key
384
+
385
+ project_path = validate_project_directory()
386
+ tracker = FileTracker()
387
+
388
+ # Create auth/custom directories
389
+ auth_dir = project_path / "auth"
390
+ custom_dir = auth_dir / "custom"
391
+ custom_dir.mkdir(parents=True, exist_ok=True)
392
+
393
+ # Create __init__.py files
394
+ auth_init = auth_dir / "__init__.py"
395
+ if not auth_init.exists():
396
+ auth_init.write_text("")
397
+
398
+ # Copy custom auth __init__.py from package
399
+ source_file = PACKAGE_DIR / "auth" / "custom" / "__init__.py"
400
+ dest_file = custom_dir / "__init__.py"
401
+ copy_file_if_not_exists(source_file, dest_file, tracker, "auth/custom/__init__.py")
402
+
403
+ # Create static/js/auth.js for custom auth
404
+ static_js_dir = project_path / "static" / "js"
405
+ static_js_dir.mkdir(parents=True, exist_ok=True)
406
+
407
+ auth_js_dest = static_js_dir / "auth.js"
408
+ auth_js_content = """\
409
+ globalThis.getAuth = async function () {
410
+ return {
411
+ token: "test-token", // TODO: implement
412
+ provider: "custom",
413
+ };
414
+ };
415
+ """
416
+
417
+ if auth_js_dest.exists():
418
+ tracker.mark_skipped("static/js/auth.js")
419
+ else:
420
+ auth_js_dest.write_text(dedent(auth_js_content))
421
+ tracker.mark_created("static/js/auth.js")
422
+
423
+ # Update .env to add "custom" to XLWINGS_AUTH_PROVIDERS
424
+ env_file = project_path / ".env"
425
+ if env_file.exists():
426
+ env_content = env_file.read_text()
427
+
428
+ # Check if XLWINGS_AUTH_PROVIDERS is commented
429
+ if "# XLWINGS_AUTH_PROVIDERS=[]" in env_content:
430
+ # Uncomment and set to ["custom"]
431
+ env_content = env_content.replace(
432
+ "# XLWINGS_AUTH_PROVIDERS=[]", 'XLWINGS_AUTH_PROVIDERS=["custom"]'
433
+ )
434
+ env_file.write_text(env_content)
435
+ tracker.mark_created(".env (updated XLWINGS_AUTH_PROVIDERS)")
436
+ elif "XLWINGS_AUTH_PROVIDERS=" in env_content:
437
+ # Line is already uncommented, parse and update
438
+ env_values = dotenv_values(env_file)
439
+ current_providers = env_values.get("XLWINGS_AUTH_PROVIDERS", "")
440
+
441
+ # Parse as JSON array (e.g., ["entraid"])
442
+ if current_providers:
443
+ try:
444
+ providers = json.loads(current_providers)
445
+ except json.JSONDecodeError:
446
+ # Fallback to comma-separated if not JSON
447
+ providers = [p.strip() for p in current_providers.split(",")]
448
+ else:
449
+ providers = []
450
+
451
+ # Add "custom" if not already present
452
+ if "custom" not in providers:
453
+ providers.append("custom")
454
+ set_key(
455
+ env_file,
456
+ "XLWINGS_AUTH_PROVIDERS",
457
+ json.dumps(providers),
458
+ quote_mode="never",
459
+ )
460
+ tracker.mark_created(".env (updated XLWINGS_AUTH_PROVIDERS)")
461
+ else:
462
+ tracker.mark_skipped(".env (custom already in XLWINGS_AUTH_PROVIDERS)")
463
+ else:
464
+ tracker.mark_skipped(".env (XLWINGS_AUTH_PROVIDERS not found)")
465
+
466
+ tracker.print_summary("Custom auth provider setup")
467
+
468
+
469
+ def add_config_command():
470
+ """Add config.py to project for customizing settings"""
471
+ project_path = validate_project_directory()
472
+ tracker = FileTracker()
473
+
474
+ # Check if config.py already exists
475
+ config_file = project_path / "config.py"
476
+ if config_file.exists():
477
+ tracker.mark_skipped("config.py (already exists)")
478
+ else:
479
+ # Create config.py template
480
+ template = dedent('''\
481
+ """This uses pydantic-settings:
482
+ https://docs.pydantic.dev/latest/concepts/pydantic_settings/
483
+ """
484
+
485
+ from xlwings_server.config import Settings as BaseSettings
486
+
487
+
488
+ class Settings(BaseSettings):
489
+ my_custom_setting: str = "default_value"
490
+
491
+ ''')
492
+
493
+ config_file.write_text(template)
494
+ tracker.mark_created("config.py")
495
+
496
+ tracker.print_summary("Config setup")
497
+
498
+
499
+ def add_auth_entraid_command():
500
+ """Add Entra ID auth provider jwks.py to project for customization"""
501
+ import json
502
+
503
+ from dotenv import dotenv_values, set_key
504
+
505
+ project_path = validate_project_directory()
506
+ tracker = FileTracker()
507
+
508
+ # Create auth/entraid directories
509
+ auth_dir = project_path / "auth"
510
+ entraid_dir = auth_dir / "entraid"
511
+ entraid_dir.mkdir(parents=True, exist_ok=True)
512
+
513
+ # Create __init__.py files
514
+ auth_init = auth_dir / "__init__.py"
515
+ if not auth_init.exists():
516
+ auth_init.write_text("")
517
+
518
+ # Copy jwks.py from package
519
+ source_file = PACKAGE_DIR / "auth" / "entraid" / "jwks.py"
520
+ dest_file = entraid_dir / "jwks.py"
521
+ copy_file_if_not_exists(source_file, dest_file, tracker, "auth/entraid/jwks.py")
522
+
523
+ # Update .env to add "entraid" to XLWINGS_AUTH_PROVIDERS
524
+ env_file = project_path / ".env"
525
+ if env_file.exists():
526
+ env_content = env_file.read_text()
527
+
528
+ # Check if XLWINGS_AUTH_PROVIDERS is commented
529
+ if "# XLWINGS_AUTH_PROVIDERS=[]" in env_content:
530
+ # Uncomment and set to ["entraid"]
531
+ env_content = env_content.replace(
532
+ "# XLWINGS_AUTH_PROVIDERS=[]", 'XLWINGS_AUTH_PROVIDERS=["entraid"]'
533
+ )
534
+ env_file.write_text(env_content)
535
+ tracker.mark_created(".env (updated XLWINGS_AUTH_PROVIDERS)")
536
+ elif "XLWINGS_AUTH_PROVIDERS=" in env_content:
537
+ # Line is already uncommented, parse and update
538
+ env_values = dotenv_values(env_file)
539
+ current_providers = env_values.get("XLWINGS_AUTH_PROVIDERS", "")
540
+
541
+ # Parse as JSON array (e.g., ["custom"])
542
+ if current_providers:
543
+ try:
544
+ providers = json.loads(current_providers)
545
+ except json.JSONDecodeError:
546
+ # Fallback to comma-separated if not JSON
547
+ providers = [p.strip() for p in current_providers.split(",")]
548
+ else:
549
+ providers = []
550
+
551
+ # Add "entraid" if not already present
552
+ if "entraid" not in providers:
553
+ providers.append("entraid")
554
+ set_key(
555
+ env_file,
556
+ "XLWINGS_AUTH_PROVIDERS",
557
+ json.dumps(providers),
558
+ quote_mode="never",
559
+ )
560
+ tracker.mark_created(".env (updated XLWINGS_AUTH_PROVIDERS)")
561
+ else:
562
+ tracker.mark_skipped(".env (entraid already in XLWINGS_AUTH_PROVIDERS)")
563
+ else:
564
+ tracker.mark_skipped(".env (XLWINGS_AUTH_PROVIDERS not found)")
565
+
566
+ tracker.print_summary("Entra ID auth provider setup")
567
+
568
+
569
+ def create_manifest_template(project_path: Path):
570
+ """Copy manifest.xml template from package to project for customization"""
571
+ source_file = PACKAGE_DIR / "templates" / "manifest.xml"
572
+ dest_file = project_path / "templates" / "manifest.xml"
573
+
574
+ if dest_file.exists():
575
+ return
576
+
577
+ # Create templates directory if it doesn't exist
578
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
579
+
580
+ # Copy manifest template
581
+ if source_file.exists():
582
+ shutil.copy(source_file, dest_file)
583
+
584
+
585
+ def add_css_command(silent: bool = False):
586
+ """Add style.css to project for customization"""
587
+ project_path = validate_project_directory()
588
+ tracker = FileTracker()
589
+
590
+ source_file = PACKAGE_DIR / "static" / "css" / "style.css"
591
+ dest_file = project_path / "static" / "css" / "style.css"
592
+
593
+ copy_file_if_not_exists(source_file, dest_file, tracker, "static/css/style.css")
594
+ if not silent:
595
+ tracker.print_summary("CSS setup")
596
+
597
+
598
+ def add_js_command(silent: bool = False):
599
+ """Add main.js to project for customization"""
600
+ project_path = validate_project_directory()
601
+ tracker = FileTracker()
602
+
603
+ source_file = PACKAGE_DIR / "static" / "js" / "main.js"
604
+ dest_file = project_path / "static" / "js" / "main.js"
605
+
606
+ copy_file_if_not_exists(source_file, dest_file, tracker, "static/js/main.js")
607
+ if not silent:
608
+ tracker.print_summary("JS setup")
609
+
610
+
611
+ def create_dotenv(project_path: Path):
612
+ """Copy .env.template from package to project as .env and set project name and secret key"""
613
+ import secrets
614
+
615
+ env_template_path = PACKAGE_DIR / ".env.template"
616
+ env_path = project_path / ".env"
617
+
618
+ if env_path.exists():
619
+ return
620
+
621
+ # Copy template
622
+ shutil.copy(env_template_path, env_path)
623
+
624
+ # Generate secret key
625
+ secret_key = secrets.token_urlsafe(32)
626
+ project_name = project_path.name
627
+
628
+ # Read the .env file
629
+ env_content = env_path.read_text()
630
+
631
+ # Uncomment and set XLWINGS_PROJECT_NAME
632
+ env_content = env_content.replace(
633
+ '# XLWINGS_PROJECT_NAME=""', f'XLWINGS_PROJECT_NAME="{project_name}"'
634
+ )
635
+
636
+ # Uncomment and set XLWINGS_SECRET_KEY
637
+ env_content = env_content.replace(
638
+ 'XLWINGS_SECRET_KEY=""', f'XLWINGS_SECRET_KEY="{secret_key}"'
639
+ )
640
+
641
+ # Write back
642
+ env_path.write_text(env_content)
643
+
644
+
645
+ def create_gitignore(project_path: Path):
646
+ """Create or update .gitignore with xlwings-server specific entries"""
647
+ gitignore_path = project_path / ".gitignore"
648
+
649
+ # xlwings-server specific entries
650
+ xlwings_entries = [
651
+ "",
652
+ "# xlwings-server",
653
+ ".env",
654
+ "certs/",
655
+ "*.pem",
656
+ ]
657
+
658
+ # Base Python entries (only used if .gitignore doesn't exist)
659
+ base_entries = [
660
+ "# Python-generated files",
661
+ "__pycache__/",
662
+ "*.py[oc]",
663
+ "build/",
664
+ "dist/",
665
+ "wheels/",
666
+ "*.egg-info",
667
+ "",
668
+ "# Virtual environments",
669
+ ".venv",
670
+ ]
671
+
672
+ if gitignore_path.exists():
673
+ # .gitignore exists - append only xlwings-server entries if not present
674
+ content = gitignore_path.read_text()
675
+
676
+ # Check if xlwings entries are already present
677
+ has_xlwings_section = "# xlwings-server" in content
678
+
679
+ if not has_xlwings_section:
680
+ # Ensure file ends with newline before appending
681
+ if content and not content.endswith("\n"):
682
+ content += "\n"
683
+
684
+ # Append xlwings-server specific entries
685
+ content += "\n".join(xlwings_entries) + "\n"
686
+ gitignore_path.write_text(content)
687
+ else:
688
+ # .gitignore doesn't exist - create with base + xlwings entries
689
+ all_entries = base_entries + xlwings_entries
690
+ gitignore_path.write_text("\n".join(all_entries) + "\n")
691
+
692
+
693
+ def create_uuids(project_path: Path | None = None):
694
+ """Generate manifest UUIDs in pyproject.toml"""
695
+ import tomlkit
696
+
697
+ project_dir = project_path or Path.cwd()
698
+ pyproject_path = project_dir / "pyproject.toml"
699
+
700
+ if not pyproject_path.exists():
701
+ # Create a minimal pyproject.toml
702
+ data = tomlkit.document()
703
+ data["tool"] = {}
704
+ data["tool"]["xlwings_server"] = {}
705
+ else:
706
+ # Read existing pyproject.toml with tomlkit to preserve formatting
707
+ content = pyproject_path.read_text()
708
+ data = tomlkit.parse(content)
709
+
710
+ # Check if UUIDs already exist
711
+ if "tool" in data and "xlwings_server" in data["tool"]:
712
+ existing_config = data["tool"]["xlwings_server"]
713
+ if "manifest_id_dev" in existing_config:
714
+ # Don't overwrite existing UUIDs
715
+ return
716
+ else:
717
+ if "tool" not in data:
718
+ data["tool"] = {}
719
+ data["tool"]["xlwings_server"] = {}
720
+
721
+ # Generate new UUIDs
722
+ manifest_ids = {
723
+ "manifest_id_dev": str(uuid.uuid4()),
724
+ "manifest_id_qa": str(uuid.uuid4()),
725
+ "manifest_id_uat": str(uuid.uuid4()),
726
+ "manifest_id_staging": str(uuid.uuid4()),
727
+ "manifest_id_prod": str(uuid.uuid4()),
728
+ }
729
+
730
+ # Add UUIDs to [tool.xlwings_server] section
731
+ for key, value in manifest_ids.items():
732
+ data["tool"]["xlwings_server"][key] = value
733
+
734
+ # Write back (tomlkit preserves formatting)
735
+ pyproject_path.write_text(tomlkit.dumps(data))
736
+
737
+
738
+ def init_command(path: str | None = None):
739
+ """Initialize project"""
740
+ # Determine project path
741
+ if path is None:
742
+ project_path = Path.cwd()
743
+
744
+ # Ask for confirmation when initializing in current directory
745
+ print(f"This will initialize an xlwings-server project in: {project_path}")
746
+ response = input("Continue? [y/N]: ").strip().lower()
747
+ if response not in ("y", "yes"):
748
+ print("Initialization cancelled.")
749
+ sys.exit(0)
750
+ else:
751
+ project_path = Path(path).resolve()
752
+
753
+ create_project_structure(project_path)
754
+ create_manifest_template(project_path)
755
+ create_dotenv(project_path)
756
+ create_gitignore(project_path)
757
+ create_uuids(project_path)
758
+
759
+ # Add CSS and JS files (reusing existing commands)
760
+ original_cwd = Path.cwd()
761
+ try:
762
+ os.chdir(project_path)
763
+
764
+ # Create empty images directory
765
+ images_dir = project_path / "static" / "images"
766
+ images_dir.mkdir(parents=True, exist_ok=True)
767
+
768
+ # Add CSS and JS (silent mode to avoid cluttering init output)
769
+ add_css_command(silent=True)
770
+ add_js_command(silent=True)
771
+ finally:
772
+ os.chdir(original_cwd)
773
+
774
+ print("Initialization complete!")
775
+
776
+
777
+ def add_azure_functions_command():
778
+ """Add Azure Functions deployment files to project"""
779
+ project_path = validate_project_directory()
780
+ tracker = FileTracker()
781
+
782
+ # Azure Functions template files are in xlwings_server/azure_functions_templates/
783
+ source_dir = PACKAGE_DIR / "azure_functions_templates"
784
+
785
+ azure_files = [
786
+ "function_app.py",
787
+ "host.json",
788
+ ".funcignore",
789
+ "local.settings.json",
790
+ ]
791
+
792
+ # Copy files with idempotency
793
+ for filename in azure_files:
794
+ source_file = source_dir / filename
795
+ dest_file = project_path / filename
796
+
797
+ if not source_file.exists():
798
+ print(f"Warning: Source file not found: {source_file}")
799
+ continue
800
+
801
+ copy_file_if_not_exists(source_file, dest_file, tracker, filename)
802
+
803
+ tracker.print_summary("Azure Functions setup")
804
+
805
+
806
+ def add_docker_command():
807
+ """Add Docker deployment files to project"""
808
+ project_path = validate_project_directory()
809
+ tracker = FileTracker()
810
+
811
+ source_dir = PACKAGE_DIR / "docker_templates"
812
+
813
+ docker_files = [
814
+ "Dockerfile",
815
+ "docker-compose.yaml",
816
+ ".dockerignore",
817
+ ]
818
+
819
+ for filename in docker_files:
820
+ source_file = source_dir / filename
821
+ dest_file = project_path / filename
822
+
823
+ if not source_file.exists():
824
+ print(f"Warning: Source file not found: {source_file}")
825
+ continue
826
+
827
+ copy_file_if_not_exists(source_file, dest_file, tracker, filename)
828
+
829
+ tracker.print_summary("Docker setup")
830
+
831
+
832
+ def migrate_command(old_project_path: str):
833
+ """Migrate from pre-1.0 project structure to new 1.0+ structure"""
834
+ import shutil
835
+
836
+ # Phase 1: Validation
837
+ print("Starting migration...")
838
+
839
+ # Validate current directory is a new project
840
+ validate_project_directory()
841
+
842
+ # Check for pyproject.toml
843
+ if not Path("pyproject.toml").exists():
844
+ print("Error: pyproject.toml not found in current directory")
845
+ print("Run this command from an initialized xlwings-server 1.0+ project")
846
+ sys.exit(1)
847
+
848
+ # Validate old project structure
849
+ old_path = validate_old_project_directory(Path(old_project_path))
850
+
851
+ tracker = FileTracker()
852
+
853
+ # Phase 2: Core File Migration
854
+ # Remove default templates (from init) and old examples (from pre-1.0)
855
+ files_to_remove = [
856
+ Path("custom_functions/functions.py"), # Default from xlwings-server init
857
+ Path("custom_scripts/scripts.py"), # Default from xlwings-server init
858
+ Path("main.py"), # Default from uv init
859
+ Path("README.md"), # Default from uv init
860
+ ]
861
+ for file_path in files_to_remove:
862
+ if file_path.exists():
863
+ file_path.unlink()
864
+
865
+ # 1. Custom functions & scripts
866
+ app_path = old_path / "app"
867
+ copy_directory_recursive(
868
+ app_path / "custom_functions",
869
+ Path("custom_functions"),
870
+ tracker,
871
+ exclude_patterns=["__pycache__", "examples.py"],
872
+ )
873
+ copy_directory_recursive(
874
+ app_path / "custom_scripts",
875
+ Path("custom_scripts"),
876
+ tracker,
877
+ exclude_patterns=["__pycache__", "examples.py"],
878
+ )
879
+
880
+ # Clean up __init__.py files - only keep lines with "import *"
881
+ for init_file in [
882
+ Path("custom_functions/__init__.py"),
883
+ Path("custom_scripts/__init__.py"),
884
+ ]:
885
+ if init_file.exists():
886
+ content = init_file.read_text()
887
+ lines = content.splitlines(keepends=True)
888
+
889
+ # Only keep lines that contain "import *" (but not .examples)
890
+ cleaned_lines = []
891
+ for line in lines:
892
+ # Keep only import * lines (excluding .examples imports)
893
+ if "import *" in line and ".examples" not in line:
894
+ cleaned_lines.append(line)
895
+
896
+ # Write cleaned content
897
+ init_file.write_text("".join(cleaned_lines))
898
+
899
+ # 2. Certificates
900
+ old_certs = old_path / "certs"
901
+ if old_certs.exists():
902
+ copy_directory_recursive(old_certs, Path("certs"), tracker)
903
+
904
+ # 3. .env file
905
+ old_env = old_path / ".env"
906
+ if old_env.exists():
907
+ shutil.copy2(old_env, ".env")
908
+ tracker.mark_created(".env")
909
+
910
+ # 4. Templates - Manifest and Taskpane
911
+ Path("templates").mkdir(exist_ok=True)
912
+
913
+ # Copy manifest.xml as-is
914
+ old_manifest = app_path / "templates" / "manifest.xml"
915
+ if old_manifest.exists():
916
+ shutil.copy2(old_manifest, "templates/manifest.xml")
917
+ tracker.mark_created("templates/manifest.xml")
918
+
919
+ # Determine which taskpane template was used in old project
920
+ old_taskpane_router = app_path / "routers" / "taskpane.py"
921
+ taskpane_template_name = None
922
+
923
+ if old_taskpane_router.exists():
924
+ # Parse the router to find the template name
925
+ router_content = old_taskpane_router.read_text()
926
+
927
+ # Look for template name when enable_examples is False
928
+ import re
929
+
930
+ # First check if there's conditional logic based on enable_examples
931
+ if "enable_examples" in router_content:
932
+ # Parse conditional: if not settings.enable_examples: name="..."
933
+ match = re.search(
934
+ r'if\s+not\s+settings\.enable_examples.*?name\s*=\s*["\']([^"\']+)["\']',
935
+ router_content,
936
+ re.DOTALL,
937
+ )
938
+ if match:
939
+ taskpane_template_name = match.group(1)
940
+
941
+ # If no conditional found, look for direct template name
942
+ if not taskpane_template_name:
943
+ # Look for: name="..." or name='...'
944
+ match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', router_content)
945
+ if match:
946
+ taskpane_template_name = match.group(1)
947
+ else:
948
+ # Look for: name=settings.something
949
+ match = re.search(r"name\s*=\s*settings\.(\w+)", router_content)
950
+ if match:
951
+ taskpane_template_name = "taskpane.html" # Default fallback
952
+
953
+ # Copy the identified taskpane template
954
+ if taskpane_template_name:
955
+ old_taskpane_template = app_path / "templates" / taskpane_template_name
956
+ if old_taskpane_template.exists():
957
+ shutil.copy2(old_taskpane_template, "templates/taskpane.html")
958
+ tracker.mark_created(
959
+ f"templates/taskpane.html (from {taskpane_template_name})"
960
+ )
961
+ else:
962
+ # Template doesn't exist, try direct copy
963
+ direct_taskpane = app_path / "templates" / "taskpane.html"
964
+ if direct_taskpane.exists():
965
+ shutil.copy2(direct_taskpane, "templates/taskpane.html")
966
+ tracker.mark_created("templates/taskpane.html")
967
+ else:
968
+ # No router found, copy taskpane.html if it exists
969
+ direct_taskpane = app_path / "templates" / "taskpane.html"
970
+ if direct_taskpane.exists():
971
+ shutil.copy2(direct_taskpane, "templates/taskpane.html")
972
+ tracker.mark_created("templates/taskpane.html")
973
+
974
+ # 5. Extract and update UUIDs
975
+ uuids = extract_uuids_from_old_config(app_path / "config.py")
976
+ if uuids:
977
+ update_uuids_in_pyproject(uuids)
978
+ tracker.mark_created("pyproject.toml (UUIDs updated)")
979
+
980
+ # Phase 3: Optional Components Detection & Migration
981
+ # Static files - CSS
982
+ old_css = app_path / "static" / "css" / "style.css"
983
+ if is_file_customized(old_css):
984
+ Path("static/css").mkdir(parents=True, exist_ok=True)
985
+ shutil.copy2(old_css, "static/css/style.css")
986
+ tracker.mark_created("static/css/style.css")
987
+
988
+ # Static files - JS
989
+ old_js = app_path / "static" / "js" / "main.js"
990
+ if is_file_customized(old_js):
991
+ Path("static/js").mkdir(parents=True, exist_ok=True)
992
+ shutil.copy2(old_js, "static/js/main.js")
993
+ tracker.mark_created("static/js/main.js")
994
+
995
+ # Static files - ribbon.js
996
+ old_ribbon_js = app_path / "static" / "js" / "ribbon.js"
997
+ if is_file_customized(old_ribbon_js):
998
+ Path("static/js").mkdir(parents=True, exist_ok=True)
999
+ shutil.copy2(old_ribbon_js, "static/js/ribbon.js")
1000
+ tracker.mark_created("static/js/ribbon.js")
1001
+
1002
+ # Static files - Images
1003
+ old_images = app_path / "static" / "images"
1004
+ if old_images.exists() and any(old_images.iterdir()):
1005
+ copy_directory_recursive(old_images, Path("static/images"), tracker)
1006
+
1007
+ # Static files - Other static content
1008
+ old_static = app_path / "static"
1009
+ if old_static.exists():
1010
+ for item in old_static.iterdir():
1011
+ # Skip already handled directories
1012
+ if item.name in ["css", "js", "images", "vendor"]:
1013
+ continue
1014
+ if item.is_dir():
1015
+ copy_directory_recursive(item, Path("static") / item.name, tracker)
1016
+ elif item.is_file():
1017
+ Path("static").mkdir(exist_ok=True)
1018
+ shutil.copy2(item, Path("static") / item.name)
1019
+ tracker.mark_created(f"static/{item.name}")
1020
+
1021
+ # Custom templates (excluding package templates and already-handled files)
1022
+ old_templates = app_path / "templates"
1023
+ # These are package templates that should not be migrated
1024
+ excluded_templates = {
1025
+ "manifest.xml", # Already handled specially
1026
+ "taskpane.html", # Already handled specially
1027
+ "_book.html", # Package template
1028
+ "alert_base.html", # Package template
1029
+ "base.html", # Package template
1030
+ "xlwings_alert.html", # Package template
1031
+ "examples", # Package examples directory
1032
+ }
1033
+
1034
+ if old_templates.exists():
1035
+ for item in old_templates.iterdir():
1036
+ if item.name in excluded_templates:
1037
+ continue
1038
+ if item.is_file():
1039
+ dest_file = Path("templates") / item.name
1040
+ shutil.copy2(item, dest_file)
1041
+ tracker.mark_created(f"templates/{item.name}")
1042
+ elif item.is_dir():
1043
+ copy_directory_recursive(item, Path("templates") / item.name, tracker)
1044
+
1045
+ # Parse and install dependencies from old requirements.in
1046
+ old_requirements_in = old_path / "requirements.in"
1047
+ if old_requirements_in.exists():
1048
+ print("\nInstalling dependencies from requirements.in...")
1049
+ requirements_content = old_requirements_in.read_text()
1050
+
1051
+ # Parse dependencies (skip comments and -r references)
1052
+ dependencies = []
1053
+ for line in requirements_content.splitlines():
1054
+ line = line.strip()
1055
+ # Skip empty lines, comments, and -r references
1056
+ if not line or line.startswith("#") or line.startswith("-r"):
1057
+ continue
1058
+ dependencies.append(line)
1059
+
1060
+ # Install dependencies using uv add
1061
+ if dependencies:
1062
+ import subprocess
1063
+
1064
+ try:
1065
+ cmd = ["uv", "add"] + dependencies
1066
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
1067
+ print(f"Successfully added: {', '.join(dependencies)}")
1068
+ tracker.mark_created(f"Dependencies: {', '.join(dependencies)}")
1069
+ except subprocess.CalledProcessError as e:
1070
+ print(f"Warning: Failed to install some dependencies: {e}")
1071
+ print(
1072
+ f"You can manually install them with: uv add {' '.join(dependencies)}"
1073
+ )
1074
+ except FileNotFoundError:
1075
+ print("Warning: 'uv' command not found.")
1076
+ print(
1077
+ f"Please manually install dependencies: uv add {' '.join(dependencies)}"
1078
+ )
1079
+
1080
+ # Phase 4: Report
1081
+ tracker.print_summary("Migration")
1082
+
1083
+
1084
+ def run_server():
1085
+ """Start the xlwings-server development server"""
1086
+ # Get project directory (where user runs the command)
1087
+ project_dir = Path.cwd()
1088
+
1089
+ # Validate project structure
1090
+ custom_functions_dir = project_dir / "custom_functions"
1091
+ custom_scripts_dir = project_dir / "custom_scripts"
1092
+
1093
+ if not custom_functions_dir.exists() or not custom_scripts_dir.exists():
1094
+ print(
1095
+ "Error: Project must have custom_functions/ and custom_scripts/ directories"
1096
+ )
1097
+ sys.exit(1)
1098
+
1099
+ # Set environment variable so app can find project directory
1100
+ # The sys.path manipulation will happen in main.py before importing user modules
1101
+ os.environ["XLWINGS_PROJECT_DIR"] = str(project_dir)
1102
+
1103
+ # Copy over required settings to wasm .env
1104
+ # This is done before starting the server to ensure wasm has up-to-date settings
1105
+ from xlwings_server.config import settings # noqa: E402
1106
+
1107
+ wasm_dir = project_dir / "wasm"
1108
+ if settings.enable_wasm and wasm_dir.exists():
1109
+ env_file = wasm_dir / ".env"
1110
+ create_wasm_settings(settings, env_file)
1111
+
1112
+ # Determine SSL certificate paths
1113
+ is_cloud = os.getenv("CODESPACES")
1114
+ ssl_keyfile_path = project_dir / "certs" / "localhost+2-key.pem"
1115
+ ssl_certfile_path = project_dir / "certs" / "localhost+2.pem"
1116
+
1117
+ ssl_keyfile = (
1118
+ str(ssl_keyfile_path) if ssl_keyfile_path.exists() and not is_cloud else None
1119
+ )
1120
+ ssl_certfile = (
1121
+ str(ssl_certfile_path) if ssl_certfile_path.exists() and not is_cloud else None
1122
+ )
1123
+
1124
+ if (ssl_keyfile is None or ssl_certfile is None) and not is_cloud:
1125
+ print(
1126
+ "NO SSL KEYFILE OR CERTFILE FOUND. RUNNING ON HTTP, NOT HTTPS!.\n"
1127
+ "THIS WILL ONLY WORK WITH VBA AND OFFICE SCRIPTS, BUT NOT WITH "
1128
+ "OFFICE.JS ADD-INS!"
1129
+ )
1130
+
1131
+ # Start uvicorn server
1132
+ # Note: We don't import settings here to avoid caching the xlwings_server module
1133
+ # before sys.path manipulation
1134
+ uvicorn.run(
1135
+ "xlwings_server.main:main_app",
1136
+ host="127.0.0.1",
1137
+ port=8000,
1138
+ reload=True,
1139
+ reload_dirs=[str(project_dir)],
1140
+ reload_includes=[".env"],
1141
+ ssl_keyfile=ssl_keyfile,
1142
+ ssl_certfile=ssl_certfile,
1143
+ )
1144
+
1145
+
1146
+ def create_wasm_settings(settings, env_file):
1147
+ settings_map = {
1148
+ "XLWINGS_LICENSE_KEY": f'"{settings.license_key}"',
1149
+ "XLWINGS_ENABLE_EXAMPLES": str(settings.enable_examples).lower(),
1150
+ "XLWINGS_ENVIRONMENT": settings.environment,
1151
+ "XLWINGS_ENABLE_TESTS": str(settings.enable_tests).lower(),
1152
+ "XLWINGS_FUNCTIONS_NAMESPACE": settings.functions_namespace,
1153
+ "XLWINGS_IS_OFFICIAL_LITE_ADDIN": str(settings.is_official_lite_addin).lower(),
1154
+ }
1155
+
1156
+ for key, value in settings_map.items():
1157
+ update_wasm_settings(key, value, env_file)
1158
+
1159
+
1160
+ def update_wasm_settings(key: str, value: str, env_file: Path):
1161
+ if env_file.exists():
1162
+ content = env_file.read_text().splitlines()
1163
+ else:
1164
+ content = []
1165
+
1166
+ key_found = False
1167
+ for i, line in enumerate(content):
1168
+ if line.startswith(f"{key}="):
1169
+ content[i] = f"{key}={value}"
1170
+ key_found = True
1171
+ break
1172
+
1173
+ if not key_found:
1174
+ content.append(f"{key}={value}")
1175
+
1176
+ env_file.parent.mkdir(parents=True, exist_ok=True)
1177
+ env_file.write_text("\n".join(content) + "\n")
1178
+
1179
+
1180
+ def build_static_command(output_dir: str = "./dist", clean: bool = False):
1181
+ """Build static files and templates for production deployment with hashed filenames."""
1182
+ project_path = validate_project_directory()
1183
+ output_path = Path(output_dir)
1184
+
1185
+ if clean and output_path.exists():
1186
+ shutil.rmtree(output_path)
1187
+ print("Output directory cleaned.")
1188
+
1189
+ output_path.mkdir(exist_ok=True)
1190
+
1191
+ # Copy static: package first, then project overlays
1192
+ _copy_folder(PACKAGE_DIR / "static", output_path / "static", "static (package)")
1193
+ _copy_folder_merge(
1194
+ project_path / "static", output_path / "static", "static (project)"
1195
+ )
1196
+
1197
+ # Copy templates: package first, then project overlays
1198
+ _copy_folder(
1199
+ PACKAGE_DIR / "templates", output_path / "templates", "templates (package)"
1200
+ )
1201
+ _copy_folder_merge(
1202
+ project_path / "templates", output_path / "templates", "templates (project)"
1203
+ )
1204
+
1205
+ # Hash static files and update references in templates
1206
+ hasher = StaticFileHasher(
1207
+ static_dir=output_path / "static", templates_dir=output_path / "templates"
1208
+ )
1209
+ hasher.process_files()
1210
+
1211
+ print(f"\nBuild complete: {output_path.resolve()}")
1212
+
1213
+
1214
+ def build_wasm_command(
1215
+ url, output_dir, create_zip=False, clean=False, environment=None
1216
+ ):
1217
+ import xlwings
1218
+ import xlwings as xw
1219
+
1220
+ logging.getLogger("httpx").setLevel(logging.WARNING)
1221
+ build_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1222
+
1223
+ # Settings overrides
1224
+ parsed = urlparse(url)
1225
+ base_url = f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
1226
+ app_path = parsed.path.rstrip("/")
1227
+
1228
+ # TODO: these env vars aren't respected anymore, probably because of the config
1229
+ # import at the top. Need to be set directly in the environment where this is
1230
+ # running, e.g., CI
1231
+ os.environ["XLWINGS_ENABLE_WASM"] = "true"
1232
+ os.environ["XLWINGS_ENABLE_SOCKETIO"] = "false"
1233
+ os.environ["XLWINGS_APP_PATH"] = app_path
1234
+ os.environ["XLWINGS_STATIC_URL_PATH"] = f"{app_path}/static"
1235
+
1236
+ if environment:
1237
+ os.environ["XLWINGS_ENVIRONMENT"] = environment
1238
+
1239
+ from fastapi.testclient import TestClient # noqa: E402
1240
+
1241
+ from xlwings_server.config import settings # noqa: E402
1242
+ from xlwings_server.main import main_app # noqa: E402
1243
+
1244
+ # Make sure settings is up-to-date
1245
+ create_wasm_settings(settings=settings, env_file=PROJECT_DIR / "wasm" / ".env")
1246
+
1247
+ # Take the license key from .env
1248
+ os.environ["XLWINGS_LICENSE_KEY"] = settings.license_key
1249
+ import xlwings.pro
1250
+
1251
+ output_dir = Path(output_dir)
1252
+ output_dir.mkdir(exist_ok=True)
1253
+
1254
+ # Clean output directory
1255
+ if clean:
1256
+ if output_dir.exists():
1257
+ for filename in os.listdir(output_dir):
1258
+ file_path = output_dir / filename
1259
+ if file_path.is_file():
1260
+ file_path.unlink()
1261
+ elif file_path.is_dir():
1262
+ shutil.rmtree(file_path)
1263
+ print("Output directory cleaned.")
1264
+
1265
+ # Endpoints
1266
+ client = TestClient(main_app)
1267
+
1268
+ route_paths = [
1269
+ "manifest.xml",
1270
+ "taskpane.html", # TODO: cover all routes from taskpane.py
1271
+ "xlwings/custom-functions-meta.json",
1272
+ "xlwings/custom-functions-code.js",
1273
+ "xlwings/pyodide.json",
1274
+ ]
1275
+
1276
+ base_path = f"{app_path}/" if app_path else "/"
1277
+ routes = [urljoin(base_path, path) for path in route_paths]
1278
+
1279
+ for ix, route in enumerate(routes):
1280
+ response = client.get(route)
1281
+ if response.status_code == 200:
1282
+ content = response.text
1283
+ filename = Path(route_paths[ix])
1284
+ if filename.name == "manifest.xml":
1285
+ content = content.replace("http://testserver", base_url)
1286
+
1287
+ file_path = output_dir / filename
1288
+ file_path.parent.mkdir(parents=True, exist_ok=True)
1289
+ file_path.write_text(content)
1290
+ else:
1291
+ print(f"Failed to fetch {route} (status code: {response.status_code})")
1292
+
1293
+ # Index.html
1294
+ index_path = output_dir / "index.html"
1295
+ index_path.write_text(f"This is an xlwings Wasm app! ({build_timestamp})")
1296
+
1297
+ print("Static site generation complete.")
1298
+
1299
+ # Copy from PACKAGE_DIR first (base files)
1300
+ _copy_folder(PACKAGE_DIR / "static", output_dir / "static", "static (package)")
1301
+ _copy_folder(PACKAGE_DIR / "wasm", output_dir / "wasm", "wasm (package)")
1302
+ _copy_folder(
1303
+ PACKAGE_DIR / "custom_functions",
1304
+ output_dir / "custom_functions",
1305
+ "custom_functions (package)",
1306
+ )
1307
+ _copy_folder(
1308
+ PACKAGE_DIR / "custom_scripts",
1309
+ output_dir / "custom_scripts",
1310
+ "custom_scripts (package)",
1311
+ )
1312
+
1313
+ # Copy from PROJECT_DIR to overwrite with user customizations
1314
+ _copy_folder_merge(
1315
+ PROJECT_DIR / "static", output_dir / "static", "static (project)"
1316
+ )
1317
+ _copy_folder_merge(PROJECT_DIR / "wasm", output_dir / "wasm", "wasm (project)")
1318
+ _copy_folder_merge(
1319
+ PROJECT_DIR / "custom_functions",
1320
+ output_dir / "custom_functions",
1321
+ "custom_functions (project)",
1322
+ )
1323
+ _copy_folder_merge(
1324
+ PROJECT_DIR / "custom_scripts",
1325
+ output_dir / "custom_scripts",
1326
+ "custom_scripts (project)",
1327
+ )
1328
+
1329
+ # .env
1330
+ try:
1331
+ deploy_key = xlwings.pro.LicenseHandler.create_deploy_key()
1332
+ except xw.LicenseError:
1333
+ deploy_key = settings.license_key
1334
+ update_wasm_settings(
1335
+ "XLWINGS_LICENSE_KEY", deploy_key, output_dir / "wasm" / ".env"
1336
+ )
1337
+ if environment:
1338
+ update_wasm_settings(
1339
+ "XLWINGS_ENVIRONMENT", environment, output_dir / "wasm" / ".env"
1340
+ )
1341
+
1342
+ # Remove unused libraries
1343
+ def remove_dir_if_exists(path: Path) -> None:
1344
+ if path.exists():
1345
+ shutil.rmtree(path)
1346
+
1347
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "socket.io")
1348
+ if not settings.enable_alpinejs_csp:
1349
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "@alpinejs")
1350
+ if settings.cdn_officejs:
1351
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "@microsoft")
1352
+ if not settings.enable_bootstrap:
1353
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "bootstrap")
1354
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "bootstrap-xlwings")
1355
+ if not settings.enable_htmx:
1356
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "htmx-ext-head-support")
1357
+ remove_dir_if_exists(
1358
+ output_dir / "static" / "vendor" / "htmx-ext-loading-states"
1359
+ )
1360
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "htmx.org")
1361
+
1362
+ def has_pyodide_requirement(requirements_file):
1363
+ if not requirements_file.exists():
1364
+ return False
1365
+ with open(requirements_file, "r") as f:
1366
+ return any("/static/vendor/pyodide/" in line for line in f)
1367
+
1368
+ if settings.cdn_pyodide:
1369
+ requirements_path = output_dir / "wasm" / "requirements.txt"
1370
+ if not has_pyodide_requirement(requirements_path):
1371
+ remove_dir_if_exists(output_dir / "static" / "vendor" / "pyodide")
1372
+
1373
+ # Remove unwanted files
1374
+ for cache_dir in (output_dir / "wasm").rglob("__pycache__"):
1375
+ remove_dir_if_exists(cache_dir)
1376
+
1377
+ for ds_store in output_dir.rglob(".DS_Store"):
1378
+ ds_store.unlink(missing_ok=True)
1379
+
1380
+ # Hash files
1381
+ hasher = StaticFileHasher(static_dir=output_dir, templates_dir=output_dir)
1382
+ hasher.process_files()
1383
+
1384
+ # ZIP file
1385
+ if create_zip:
1386
+ zip_filename = output_dir / f"xlwings_wasm_{build_timestamp}.zip"
1387
+
1388
+ try:
1389
+ with zipfile.ZipFile(zip_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
1390
+ for file_path in output_dir.rglob("*"):
1391
+ if file_path.is_file() and file_path != zip_filename:
1392
+ arcname = file_path.relative_to(output_dir)
1393
+ zipf.write(file_path, arcname)
1394
+ print(f"Created zip file: {zip_filename}")
1395
+ except Exception as e:
1396
+ print(f"Error creating zip file: {e}")
1397
+
1398
+
1399
+ def main():
1400
+ """Entry point for xlwings-server CLI"""
1401
+ parser = argparse.ArgumentParser(
1402
+ description="xlwings Server - Modern Excel add-ins with Python"
1403
+ )
1404
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
1405
+
1406
+ # Init command
1407
+ init_parser = subparsers.add_parser(
1408
+ "init",
1409
+ help="Initialize project",
1410
+ )
1411
+ init_parser.add_argument(
1412
+ "path",
1413
+ nargs="?",
1414
+ default=None,
1415
+ help="Project path (default: current directory). Use '.' for current directory or specify a path to create a new project.",
1416
+ )
1417
+
1418
+ # Add command
1419
+ add_parser = subparsers.add_parser("add", help="Add optional components")
1420
+ add_subparsers = add_parser.add_subparsers(
1421
+ dest="add_category", help="Component categories"
1422
+ )
1423
+
1424
+ # azure subcommand (with nested functions subcommand)
1425
+ azure_parser = add_subparsers.add_parser("azure", help="Azure integrations")
1426
+ azure_subparsers = azure_parser.add_subparsers(
1427
+ dest="azure_command", help="Azure services"
1428
+ )
1429
+ azure_subparsers.add_parser(
1430
+ "functions", help="Add Azure Functions deployment files"
1431
+ )
1432
+
1433
+ # model subcommand (with nested user subcommand)
1434
+ model_parser = add_subparsers.add_parser("model", help="Data models")
1435
+ model_subparsers = model_parser.add_subparsers(
1436
+ dest="model_command", help="Model types"
1437
+ )
1438
+ model_subparsers.add_parser("user", help="Add user model for customization")
1439
+
1440
+ # auth subcommand (with nested custom and entraid subcommands)
1441
+ auth_parser = add_subparsers.add_parser("auth", help="Authentication providers")
1442
+ auth_subparsers = auth_parser.add_subparsers(
1443
+ dest="auth_command", help="Auth provider types"
1444
+ )
1445
+ auth_subparsers.add_parser(
1446
+ "custom", help="Add custom auth provider for customization"
1447
+ )
1448
+ auth_subparsers.add_parser(
1449
+ "entraid", help="Add Entra ID auth provider jwks.py for customization"
1450
+ )
1451
+
1452
+ # docker subcommand (standalone)
1453
+ add_subparsers.add_parser("docker", help="Add Docker deployment files")
1454
+
1455
+ # router subcommand (standalone, no nesting needed)
1456
+ add_subparsers.add_parser("router", help="Add routers directory and sample router")
1457
+
1458
+ # css subcommand (standalone)
1459
+ add_subparsers.add_parser("css", help="Add style.css for customization")
1460
+
1461
+ # js subcommand (standalone)
1462
+ add_subparsers.add_parser("js", help="Add main.js for customization")
1463
+
1464
+ # config subcommand (standalone)
1465
+ add_subparsers.add_parser("config", help="Add config.py for extending settings")
1466
+
1467
+ # Build command with subcommands
1468
+ build_parser = subparsers.add_parser("build", help="Build commands for deployment")
1469
+ build_subparsers = build_parser.add_subparsers(
1470
+ dest="build_command", help="Build targets"
1471
+ )
1472
+
1473
+ # build static subcommand
1474
+ build_static_parser = build_subparsers.add_parser(
1475
+ "static", help="Build static files and templates with hashed filenames"
1476
+ )
1477
+ build_static_parser.add_argument(
1478
+ "-o",
1479
+ "--output-dir",
1480
+ help="Output directory path (default: ./dist)",
1481
+ type=str,
1482
+ default="./dist",
1483
+ )
1484
+ build_static_parser.add_argument(
1485
+ "--no-clean",
1486
+ help="Don't clean the output directory before building",
1487
+ action="store_true",
1488
+ )
1489
+
1490
+ # build wasm subcommand
1491
+ build_wasm_parser = build_subparsers.add_parser(
1492
+ "wasm", help="Build xlwings Wasm distribution"
1493
+ )
1494
+ build_wasm_parser.add_argument(
1495
+ "url", help="URL of where the xlwings Wasm app will be hosted"
1496
+ )
1497
+ build_wasm_parser.add_argument(
1498
+ "-o",
1499
+ "--output-dir",
1500
+ help="Output directory path (default: ./dist)",
1501
+ type=str,
1502
+ default="./dist",
1503
+ )
1504
+ build_wasm_parser.add_argument(
1505
+ "-z",
1506
+ "--create-zip",
1507
+ help="Create zip archive in addition to the static files",
1508
+ action="store_true",
1509
+ )
1510
+ build_wasm_parser.add_argument(
1511
+ "--no-clean",
1512
+ help="Don't clean the output directory before building",
1513
+ action="store_true",
1514
+ )
1515
+ build_wasm_parser.add_argument(
1516
+ "-e",
1517
+ "--environment",
1518
+ help="Sets XLWINGS_ENVIRONMENT (default: value from .env)",
1519
+ type=str,
1520
+ )
1521
+
1522
+ # Migrate command
1523
+ migrate_parser = subparsers.add_parser(
1524
+ "migrate", help="Migrate from pre-1.0 project structure"
1525
+ )
1526
+ migrate_parser.add_argument(
1527
+ "old_project_path",
1528
+ help="Path to old xlwings-server project (directory containing app/)",
1529
+ )
1530
+
1531
+ args = parser.parse_args()
1532
+
1533
+ if args.command == "init":
1534
+ init_command(args.path)
1535
+ elif args.command == "add":
1536
+ if args.add_category == "azure":
1537
+ if args.azure_command == "functions":
1538
+ add_azure_functions_command()
1539
+ else:
1540
+ print("Error: Please specify Azure service (e.g., functions)")
1541
+ sys.exit(1)
1542
+ elif args.add_category == "model":
1543
+ if args.model_command == "user":
1544
+ add_model_user_command()
1545
+ else:
1546
+ print("Error: Please specify model type (e.g., user)")
1547
+ sys.exit(1)
1548
+ elif args.add_category == "auth":
1549
+ if args.auth_command == "custom":
1550
+ add_auth_custom_command()
1551
+ elif args.auth_command == "entraid":
1552
+ add_auth_entraid_command()
1553
+ else:
1554
+ print("Error: Please specify auth provider (e.g., custom, entraid)")
1555
+ sys.exit(1)
1556
+ elif args.add_category == "docker":
1557
+ add_docker_command()
1558
+ elif args.add_category == "router":
1559
+ add_router_command()
1560
+ elif args.add_category == "css":
1561
+ add_css_command()
1562
+ elif args.add_category == "js":
1563
+ add_js_command()
1564
+ elif args.add_category == "config":
1565
+ add_config_command()
1566
+ else:
1567
+ print("Error: Please specify what to add")
1568
+ print("Available: azure, docker, model, auth, router, css, js, config")
1569
+ sys.exit(1)
1570
+ elif args.command == "build":
1571
+ if args.build_command == "static":
1572
+ build_static_command(output_dir=args.output_dir, clean=not args.no_clean)
1573
+ elif args.build_command == "wasm":
1574
+ build_wasm_command(
1575
+ url=args.url,
1576
+ output_dir=args.output_dir,
1577
+ create_zip=args.create_zip,
1578
+ clean=not args.no_clean,
1579
+ environment=args.environment,
1580
+ )
1581
+ else:
1582
+ print("Error: Please specify build target (e.g., static, wasm)")
1583
+ sys.exit(1)
1584
+ elif args.command == "migrate":
1585
+ migrate_command(args.old_project_path)
1586
+ else:
1587
+ # Default: run server
1588
+ run_server()
1589
+
1590
+
1591
+ if __name__ == "__main__":
1592
+ main()