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.
- xlwings_server/.env.template +145 -0
- xlwings_server/__init__.py +12 -0
- xlwings_server/_version.py +34 -0
- xlwings_server/auth/__init__.py +0 -0
- xlwings_server/auth/custom/__init__.py +26 -0
- xlwings_server/auth/entraid/__init__.py +131 -0
- xlwings_server/auth/entraid/jwks.py +10 -0
- xlwings_server/azure_functions_templates/.funcignore +28 -0
- xlwings_server/azure_functions_templates/function_app.py +28 -0
- xlwings_server/azure_functions_templates/host.json +22 -0
- xlwings_server/azure_functions_templates/local.settings.json +8 -0
- xlwings_server/build_utils/__init__.py +9 -0
- xlwings_server/build_utils/static_file_hasher.py +212 -0
- xlwings_server/cli.py +1592 -0
- xlwings_server/config.py +228 -0
- xlwings_server/custom_functions/__init__.py +8 -0
- xlwings_server/custom_functions/examples.py +177 -0
- xlwings_server/custom_scripts/__init__.py +8 -0
- xlwings_server/custom_scripts/examples.py +94 -0
- xlwings_server/databases.py +19 -0
- xlwings_server/dependencies.py +126 -0
- xlwings_server/docker_templates/.dockerignore +15 -0
- xlwings_server/docker_templates/Dockerfile +60 -0
- xlwings_server/docker_templates/docker-compose.yaml +32 -0
- xlwings_server/hotreload.py +59 -0
- xlwings_server/main.py +242 -0
- xlwings_server/models/__init__.py +14 -0
- xlwings_server/models/user.py +53 -0
- xlwings_server/object_handles.py +142 -0
- xlwings_server/routers/__init__.py +0 -0
- xlwings_server/routers/manifest.py +82 -0
- xlwings_server/routers/root.py +16 -0
- xlwings_server/routers/socketio.py +69 -0
- xlwings_server/routers/taskpane.py +12 -0
- xlwings_server/routers/xlwings.py +197 -0
- xlwings_server/security_headers.json +53 -0
- xlwings_server/serializers/__init__.py +25 -0
- xlwings_server/serializers/default_serializer.py +19 -0
- xlwings_server/serializers/dictionary_serializer.py +25 -0
- xlwings_server/serializers/framework.py +50 -0
- xlwings_server/serializers/numpy_serializer.py +26 -0
- xlwings_server/serializers/pandas_serializer.py +95 -0
- xlwings_server/static/css/core.css +28 -0
- xlwings_server/static/css/style.css +0 -0
- xlwings_server/static/images/favicon.png +0 -0
- xlwings_server/static/images/xlwings-16.png +0 -0
- xlwings_server/static/images/xlwings-32.png +0 -0
- xlwings_server/static/images/xlwings-64.png +0 -0
- xlwings_server/static/images/xlwings-80.png +0 -0
- xlwings_server/static/js/auth.js +13 -0
- xlwings_server/static/js/config.js +4 -0
- xlwings_server/static/js/core/alpinejs-csp-boilerplate.js +11 -0
- xlwings_server/static/js/core/bootstrap-customizations.js +7 -0
- xlwings_server/static/js/core/custom-functions-code.js +296 -0
- xlwings_server/static/js/core/examples.js +62 -0
- xlwings_server/static/js/core/hotreload.js +3 -0
- xlwings_server/static/js/core/htmx-handlers.js +86 -0
- xlwings_server/static/js/core/officejs-history-fix-part1.js +3 -0
- xlwings_server/static/js/core/officejs-history-fix-part2.js +2 -0
- xlwings_server/static/js/core/reload-custom-functions.js +79 -0
- xlwings_server/static/js/core/socketio-handlers.js +34 -0
- xlwings_server/static/js/core/xlwings-alert.js +22 -0
- xlwings_server/static/js/core/xlwingsjs/alert.js +85 -0
- xlwings_server/static/js/core/xlwingsjs/auth.js +63 -0
- xlwings_server/static/js/core/xlwingsjs/sheet-buttons.js +133 -0
- xlwings_server/static/js/core/xlwingsjs/utils.js +119 -0
- xlwings_server/static/js/core/xlwingsjs/wasm.js +131 -0
- xlwings_server/static/js/core/xlwingsjs/xlwings.js +1060 -0
- xlwings_server/static/js/main.js +0 -0
- xlwings_server/static/js/ribbon.js +17 -0
- xlwings_server/static/vendor/@alpinejs/LICENSE +21 -0
- xlwings_server/static/vendor/@alpinejs/csp/dist/cdn.min.js +7 -0
- xlwings_server/static/vendor/@microsoft/office-js/LICENSE.md +76 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/af-za/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/agaveerrorux.js +18 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/agavedefaulticon32x32.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/agavedefaulticon96x96.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/businessbarclose_16x16x32.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/dropdownarrow_16x16x32.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/ellipsis_16x16x32.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/miniinfoblue_16x16x32.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/moe_default_icon.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/moe_status_icons.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/office.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/images/refresh_16x16x32.png +0 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/index.html +16 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/agaveerrorux/style/agaveerrorux.css +482 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/am-et/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ae/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-bh/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-dz/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-eg/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-iq/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-jo/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-kw/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-lb/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ly/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ma/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-om/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-qa/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-sa/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-sy/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-tn/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ar-ye/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ariatelemetry/aria-web-telemetry-2.8.0.min.js +2 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ariatelemetry/aria-web-telemetry-2.9.0.min.js +2 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ariatelemetry/aria-web-telemetry.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/az-latn-az/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/be-by/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/bg-bg/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/bn-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/bs-latn-ba/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ca-es/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/cs-cz/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/cy-gb/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/da-dk/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/de-at/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/de-ch/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/de-de/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/de-li/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/de-lu/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/el-gr/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-029/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-au/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-bz/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-ca/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-gb/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-ie/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-in/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-jm/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-my/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-nz/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-ph/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-sg/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-tt/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-us/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-za/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/en-zw/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-ar/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-bo/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-cl/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-co/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-cr/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-do/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-ec/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-es/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-gt/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-hn/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-mx/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-ni/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-pa/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-pe/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-pr/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-py/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-sv/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-us/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-uy/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es-ve/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/es6-promise.js +5 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/et-ee/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/eu-es/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-15.01.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-15.02.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-15.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-mac-16.00-core.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-mac-16.00.js +25 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-web-16.00-core.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-web-16.00.js +25 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-win32-16.00.js +19 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-win32-16.01-core.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-win32-16.01.js +25 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excel-winrt-16.00.js +25 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excelios-15.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excelwebapp-15.01.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excelwebapp-15.02.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/excelwebapp-15.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fa-ir/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fi-fi/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fil-ph/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fr-be/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fr-ca/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fr-ch/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fr-fr/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fr-lu/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/fr-mc/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ga-ie/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/gl-es/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/gu-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/he-il/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/hi-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/hr-ba/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/hr-hr/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/html2canvas.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/hu-hu/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/hy-am/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/id-id/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/is-is/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/it-ch/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/it-it/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ja-jp/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ka-ge/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/kk-kz/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/km-kh/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/kn-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ko-kr/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/lb-lu/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/lo-la/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/lt-lt/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/lv-lv/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/mk-mk/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ml-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/mn-mn/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/mr-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ms-bn/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ms-my/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/mt-mt/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/nb-no/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ne-np/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/nl-be/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/nl-nl/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/nn-no/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/o15apptofilemappingtable.js +11 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/office-vsdoc.js +28596 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/office.js +84 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/pl-pl/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/pt-br/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/pt-pt/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ro-ro/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ru-ru/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/si-lk/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sk-sk/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sl-si/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sq-al/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sr-cyrl-cs/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sr-cyrl-rs/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sr-latn-cs/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sr-latn-rs/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sv-fi/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sv-se/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/sw-ke/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ta-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/te-in/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/telemetry/oteljs.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/telemetry/oteljs_agave.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/th-th/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/tr-tr/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/uk-ua/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/ur-pk/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/vi-vn/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/webauth/webauth.browserauth.js +77 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/webauth/webauth.implicit.js +35 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/zh-cn/office_strings.js +1 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/zh-hk/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/zh-mo/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/zh-sg/office_strings.js +8 -0
- xlwings_server/static/vendor/@microsoft/office-js/dist/zh-tw/office_strings.js +1 -0
- xlwings_server/static/vendor/axios/dist/axios.min.js +3 -0
- xlwings_server/static/vendor/axios/dist/axios.min.js.map +1 -0
- xlwings_server/static/vendor/bootstrap/LICENSE +21 -0
- xlwings_server/static/vendor/bootstrap/dist/js/bootstrap.bundle.min.js +7 -0
- xlwings_server/static/vendor/bootstrap/dist/js/bootstrap.bundle.min.js.map +1 -0
- xlwings_server/static/vendor/bootstrap-xlwings/dist/bootstrap-xlwings.min.css +12 -0
- xlwings_server/static/vendor/bootstrap-xlwings/dist/bootstrap-xlwings.min.css.map +1 -0
- xlwings_server/static/vendor/htmx-ext-head-support/head-support.js +144 -0
- xlwings_server/static/vendor/htmx-ext-loading-states/loading-states.js +184 -0
- xlwings_server/static/vendor/htmx.org/LICENSE +13 -0
- xlwings_server/static/vendor/htmx.org/dist/htmx.min.js +1 -0
- xlwings_server/static/vendor/socket.io/LICENSE +22 -0
- xlwings_server/static/vendor/socket.io/client-dist/socket.io.min.js +7 -0
- xlwings_server/static/vendor/socket.io/client-dist/socket.io.min.js.map +1 -0
- xlwings_server/templates/_book.html +8 -0
- xlwings_server/templates/alert_base.html +16 -0
- xlwings_server/templates/base.html +117 -0
- xlwings_server/templates/examples/alpine/README.md +26 -0
- xlwings_server/templates/examples/alpine/taskpane.html +47 -0
- xlwings_server/templates/examples/auth/README.md +38 -0
- xlwings_server/templates/examples/auth/protected.html +8 -0
- xlwings_server/templates/examples/auth/public.html +11 -0
- xlwings_server/templates/examples/excel_object_model/README.md +49 -0
- xlwings_server/templates/examples/excel_object_model/add_name_form.html +27 -0
- xlwings_server/templates/examples/hello_world/README.md +9 -0
- xlwings_server/templates/examples/hello_world/taskpane_hello.html +24 -0
- xlwings_server/templates/examples/htmx_form/README.md +44 -0
- xlwings_server/templates/examples/htmx_form/_greeting.html +6 -0
- xlwings_server/templates/examples/htmx_form/taskpane_htmx_form.html +21 -0
- xlwings_server/templates/examples/live_form_validation/README.md +60 -0
- xlwings_server/templates/examples/live_form_validation/add_name_form.html +33 -0
- xlwings_server/templates/examples/multi_app/README.md +34 -0
- xlwings_server/templates/examples/multi_app/taskpane1.html +7 -0
- xlwings_server/templates/examples/multi_app/taskpane2.html +7 -0
- xlwings_server/templates/examples/multi_app/taskpane_loader.html +5 -0
- xlwings_server/templates/examples/navigation/README.md +28 -0
- xlwings_server/templates/examples/navigation/_navigation.html +16 -0
- xlwings_server/templates/examples/navigation/taskpane_one.html +8 -0
- xlwings_server/templates/examples/navigation/taskpane_three.html +8 -0
- xlwings_server/templates/examples/navigation/taskpane_two.html +8 -0
- xlwings_server/templates/examples/pictures/README.md +42 -0
- xlwings_server/templates/examples/pictures/_picture.html +4 -0
- xlwings_server/templates/examples/pictures/taskpane_pictures.html +26 -0
- xlwings_server/templates/manifest.xml +155 -0
- xlwings_server/templates/taskpane.html +1 -0
- xlwings_server/templates/xlwings_alert.html +27 -0
- xlwings_server/templates.py +61 -0
- xlwings_server/utils.py +32 -0
- xlwings_server/wasm/__init__.py +0 -0
- xlwings_server/wasm/config.py +24 -0
- xlwings_server/wasm/main.py +236 -0
- xlwings_server/wasm/requirements.txt +5 -0
- xlwings_server-1.1.0.dist-info/METADATA +61 -0
- xlwings_server-1.1.0.dist-info/RECORD +313 -0
- xlwings_server-1.1.0.dist-info/WHEEL +4 -0
- xlwings_server-1.1.0.dist-info/entry_points.txt +2 -0
- 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()
|