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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Dockerfile for xlwings Server
|
|
2
|
+
# Use a Python image with uv pre-installed
|
|
3
|
+
FROM ghcr.io/astral-sh/uv:0.9.26-python3.14-trixie-slim
|
|
4
|
+
|
|
5
|
+
# Setup a non-root user
|
|
6
|
+
RUN groupadd --system --gid 999 nonroot \
|
|
7
|
+
&& useradd --system --gid 999 --uid 999 --create-home nonroot
|
|
8
|
+
|
|
9
|
+
# Install the project into `/app`
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
|
|
12
|
+
# Enable bytecode compilation
|
|
13
|
+
ENV UV_COMPILE_BYTECODE=1
|
|
14
|
+
|
|
15
|
+
# Copy from the cache instead of linking since it's a mounted volume
|
|
16
|
+
ENV UV_LINK_MODE=copy
|
|
17
|
+
|
|
18
|
+
# Ensure installed tools can be executed out of the box
|
|
19
|
+
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
|
20
|
+
|
|
21
|
+
# Copy dependency files first for better layer caching
|
|
22
|
+
COPY pyproject.toml uv.lock ./
|
|
23
|
+
|
|
24
|
+
# Install the project's dependencies using the lockfile and settings
|
|
25
|
+
RUN uv sync --locked --no-install-project --no-dev
|
|
26
|
+
|
|
27
|
+
# Then, add the rest of the project source code and install it
|
|
28
|
+
# Installing separately from its dependencies allows optimal layer caching
|
|
29
|
+
COPY . /app
|
|
30
|
+
RUN uv sync --locked --no-dev
|
|
31
|
+
|
|
32
|
+
# Build static files with hashed filenames for cache-busting
|
|
33
|
+
RUN uv run xlwings-server build static
|
|
34
|
+
|
|
35
|
+
# Place executables in the environment at the front of the path
|
|
36
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
37
|
+
|
|
38
|
+
# Python settings
|
|
39
|
+
ENV PYTHONUNBUFFERED=1
|
|
40
|
+
|
|
41
|
+
# Default port and workers
|
|
42
|
+
ENV PORT=8000
|
|
43
|
+
ENV WORKERS=1
|
|
44
|
+
EXPOSE 8000
|
|
45
|
+
|
|
46
|
+
# Reset the entrypoint, don't invoke `uv`
|
|
47
|
+
ENTRYPOINT []
|
|
48
|
+
|
|
49
|
+
# Use the non-root user to run our application
|
|
50
|
+
USER nonroot
|
|
51
|
+
|
|
52
|
+
# Uses shell to set PORT and WORKERS, then exec for proper signal handling
|
|
53
|
+
CMD ["sh", "-c", "\
|
|
54
|
+
exec uvicorn \
|
|
55
|
+
--host 0.0.0.0 \
|
|
56
|
+
--port $PORT \
|
|
57
|
+
--workers $WORKERS \
|
|
58
|
+
--access-log \
|
|
59
|
+
xlwings_server.main:main_app \
|
|
60
|
+
"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# This is for local development
|
|
2
|
+
services:
|
|
3
|
+
xlwings-server:
|
|
4
|
+
build: .
|
|
5
|
+
# Allow the server to run with or without certs
|
|
6
|
+
command: >
|
|
7
|
+
sh -c '
|
|
8
|
+
SSL_OPTS=""
|
|
9
|
+
if [ -f /app/certs/localhost+2-key.pem ] && [ -f /app/certs/localhost+2.pem ]; then
|
|
10
|
+
SSL_OPTS="--ssl-keyfile /app/certs/localhost+2-key.pem --ssl-certfile /app/certs/localhost+2.pem"
|
|
11
|
+
fi
|
|
12
|
+
exec uvicorn \
|
|
13
|
+
--host 0.0.0.0 \
|
|
14
|
+
--port 8000 \
|
|
15
|
+
--reload \
|
|
16
|
+
--access-log \
|
|
17
|
+
$$SSL_OPTS \
|
|
18
|
+
xlwings_server.main:main_app
|
|
19
|
+
'
|
|
20
|
+
ports:
|
|
21
|
+
- "8000:8000"
|
|
22
|
+
env_file:
|
|
23
|
+
- .env
|
|
24
|
+
restart: "no"
|
|
25
|
+
init: true # Helps with reload
|
|
26
|
+
volumes:
|
|
27
|
+
- ./certs:/app/certs
|
|
28
|
+
- ./custom_functions:/app/custom_functions
|
|
29
|
+
- ./custom_scripts:/app/custom_scripts
|
|
30
|
+
- ./dist:/app/dist
|
|
31
|
+
- ./static:/app/static
|
|
32
|
+
- ./templates:/app/templates
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Uvicorn reloads the app when the Python code is changed. So, we need to reload the
|
|
3
|
+
browser when the app starts to show any changes caused the Python code.
|
|
4
|
+
But, restarting the app takes a lot of time (especially in Docker), so we
|
|
5
|
+
watch HTML, CSS, and JS files separately via Watchfiles. If these files change, we just
|
|
6
|
+
reload the browser without having to restart the Python app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from watchfiles import Change, DefaultFilter, awatch
|
|
12
|
+
|
|
13
|
+
from .config import settings
|
|
14
|
+
|
|
15
|
+
browser_reload_triggered_by_backend = False
|
|
16
|
+
watching_frontend_files = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WebFilter(DefaultFilter):
|
|
20
|
+
allowed_extensions = (".html", ".css", ".js", ".py", ".txt", ".env")
|
|
21
|
+
|
|
22
|
+
def __call__(self, change: Change, path: str) -> bool:
|
|
23
|
+
if not super().__call__(change, path):
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
path = Path(path)
|
|
27
|
+
if path.suffix not in self.allowed_extensions:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
# Allow HTML/CSS/JS files anywhere
|
|
31
|
+
if path.suffix in (".html", ".css", ".js"):
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
# xlwings Wasm
|
|
35
|
+
if settings.enable_wasm and path.suffix in (".py", ".txt", ".env"):
|
|
36
|
+
allowed_dirs = ("wasm", "custom_scripts", "custom_functions")
|
|
37
|
+
return any(dir_name in path.parts for dir_name in allowed_dirs)
|
|
38
|
+
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def watch_frontend_files(sio, directory):
|
|
43
|
+
async for changes in awatch(
|
|
44
|
+
directory,
|
|
45
|
+
watch_filter=WebFilter(),
|
|
46
|
+
):
|
|
47
|
+
await sio.emit("reload")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def start_browser_reload_watcher(sio, directory):
|
|
51
|
+
"""Needs to be called from the sio connect event on the backend"""
|
|
52
|
+
global browser_reload_triggered_by_backend
|
|
53
|
+
global watching_frontend_files
|
|
54
|
+
if not browser_reload_triggered_by_backend and not settings.enable_wasm:
|
|
55
|
+
await sio.emit("reload")
|
|
56
|
+
browser_reload_triggered_by_backend = True
|
|
57
|
+
if not watching_frontend_files:
|
|
58
|
+
sio.start_background_task(watch_frontend_files, sio=sio, directory=directory)
|
|
59
|
+
watching_frontend_files = True
|
xlwings_server/main.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from functools import cache
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import socketio
|
|
9
|
+
from fastapi import FastAPI, status
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.responses import PlainTextResponse
|
|
12
|
+
from fastapi.staticfiles import StaticFiles
|
|
13
|
+
from xlwings import XlwingsError
|
|
14
|
+
|
|
15
|
+
# CRITICAL: Setup sys.path for user overrides BEFORE importing user modules
|
|
16
|
+
# This is done here (not in CLI) so it works when uvicorn imports this module
|
|
17
|
+
if project_dir := os.getenv("XLWINGS_PROJECT_DIR"):
|
|
18
|
+
project_dir = Path(project_dir)
|
|
19
|
+
if str(project_dir) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(project_dir))
|
|
21
|
+
|
|
22
|
+
from xlwings_server.config import PACKAGE_DIR, PROJECT_DIR, settings
|
|
23
|
+
from xlwings_server.object_handles import ObjectCacheConverter
|
|
24
|
+
from xlwings_server.routers import socketio as socketio_router
|
|
25
|
+
from xlwings_server.routers.manifest import router as manifest_router
|
|
26
|
+
from xlwings_server.routers.root import router as root_router
|
|
27
|
+
from xlwings_server.routers.taskpane import router as taskpane_router
|
|
28
|
+
from xlwings_server.routers.xlwings import router as xlwings_router
|
|
29
|
+
from xlwings_server.templates import templates
|
|
30
|
+
|
|
31
|
+
# Logging
|
|
32
|
+
logging.basicConfig(level=settings.log_level.upper())
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
logger.info(f"Running in '{'Wasm' if settings.enable_wasm else 'Server'}' mode.")
|
|
36
|
+
|
|
37
|
+
# App
|
|
38
|
+
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Starlette's url_for returns fully qualified URLs causing issues if the reverse proxy
|
|
42
|
+
# handles TLS and the app runs on http (https://github.com/encode/starlette/issues/843)
|
|
43
|
+
def url_for(name: str, **path_params):
|
|
44
|
+
"""Wrapper around url_path_for that strips leading slashes from path parameters"""
|
|
45
|
+
if name == "static" and "path" in path_params:
|
|
46
|
+
# Strip leading slash from static file paths to avoid double slashes
|
|
47
|
+
path_params["path"] = path_params["path"].lstrip("/")
|
|
48
|
+
return app.url_path_for(name, **path_params)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
templates.env.globals["url_for"] = url_for
|
|
52
|
+
|
|
53
|
+
# Register Converter
|
|
54
|
+
ObjectCacheConverter.register(object, "object", "obj")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Custom StaticFiles with file-level override support
|
|
58
|
+
class OverridableStaticFiles(StaticFiles):
|
|
59
|
+
"""StaticFiles that checks user directory first, then falls back to package directory"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, package_directory: Path, user_directory: Path, **kwargs):
|
|
62
|
+
self.package_dir = package_directory
|
|
63
|
+
self.user_dir = user_directory
|
|
64
|
+
super().__init__(directory=str(package_directory), **kwargs)
|
|
65
|
+
|
|
66
|
+
def lookup_path(self, path: str) -> tuple[str, os.stat_result | None]:
|
|
67
|
+
# Check user directory first
|
|
68
|
+
user_path = self.user_dir / path
|
|
69
|
+
if user_path.is_file():
|
|
70
|
+
return str(user_path), os.stat(user_path)
|
|
71
|
+
# Fallback to package directory
|
|
72
|
+
return super().lookup_path(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# CORS: Office Scripts and custom functions in Excel on the web require CORS
|
|
76
|
+
# Using app.add_middleware won't add the CORS headers if you handle the root "Exception"
|
|
77
|
+
# in an exception handler (it would require a specific exception type).
|
|
78
|
+
cors_app = CORSMiddleware(
|
|
79
|
+
app=app,
|
|
80
|
+
allow_origins=settings.cors_allow_origins,
|
|
81
|
+
allow_methods=["POST"],
|
|
82
|
+
allow_headers=["*"],
|
|
83
|
+
allow_credentials=False,
|
|
84
|
+
)
|
|
85
|
+
main_app = cors_app
|
|
86
|
+
|
|
87
|
+
# Socket.io
|
|
88
|
+
if settings.enable_socketio:
|
|
89
|
+
sio_app = socketio.ASGIApp(
|
|
90
|
+
socketio_router.sio,
|
|
91
|
+
# Only forward ASGI traffic if there's no message queue and hence 1 worker setup
|
|
92
|
+
cors_app if not settings.socketio_message_queue_url else None,
|
|
93
|
+
socketio_path=f"{settings.app_path}/socket.io",
|
|
94
|
+
)
|
|
95
|
+
main_app = sio_app if not settings.socketio_message_queue_url else cors_app
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Routers
|
|
99
|
+
app.include_router(root_router)
|
|
100
|
+
app.include_router(xlwings_router)
|
|
101
|
+
app.include_router(taskpane_router)
|
|
102
|
+
app.include_router(manifest_router)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# User routers (optional custom router)
|
|
106
|
+
try:
|
|
107
|
+
from routers import custom
|
|
108
|
+
|
|
109
|
+
if hasattr(custom, "router"):
|
|
110
|
+
app.include_router(custom.router)
|
|
111
|
+
logger.info("Registered custom user router")
|
|
112
|
+
except ModuleNotFoundError:
|
|
113
|
+
logger.debug("No custom router found (routers/custom.py)")
|
|
114
|
+
except Exception:
|
|
115
|
+
logger.exception("Failed to load custom router from routers/custom.py")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Security headers
|
|
119
|
+
@cache
|
|
120
|
+
def read_security_headers():
|
|
121
|
+
with open(settings.base_dir / "security_headers.json", "r") as f:
|
|
122
|
+
data = json.load(f)
|
|
123
|
+
return data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.middleware("http")
|
|
127
|
+
async def add_security_headers(request, call_next):
|
|
128
|
+
# https://owasp.org/www-project-secure-headers/index.html#configuration-proposal
|
|
129
|
+
# https://owasp.org/www-project-secure-headers/ci/headers_add.json
|
|
130
|
+
response = await call_next(request)
|
|
131
|
+
if not settings.add_security_headers and settings.environment == "dev":
|
|
132
|
+
# Prevent caching in dev even if security headers are switched off
|
|
133
|
+
response.headers["Cache-Control"] = "no-store, max-age=0"
|
|
134
|
+
if settings.add_security_headers:
|
|
135
|
+
data = read_security_headers()
|
|
136
|
+
|
|
137
|
+
# Extract file extension from request URL
|
|
138
|
+
file_ext = request.url.path.split(".")[-1].lower()
|
|
139
|
+
image_ext = ("jpg", "jpeg", "png", "gif", "bmp", "webp", "svg")
|
|
140
|
+
|
|
141
|
+
for header in data["headers"]:
|
|
142
|
+
# Excel on Windows doesn't display the icons in the ribbon otherwise
|
|
143
|
+
if header["name"] == "Cache-Control" and file_ext in image_ext:
|
|
144
|
+
continue
|
|
145
|
+
if header["name"] not in (
|
|
146
|
+
"Permissions-Policy", # experimental
|
|
147
|
+
"Clear-Site-Data", # too aggressive
|
|
148
|
+
"Content-Security-Policy", # provide via XLWINGS_CUSTOM_HEADERS
|
|
149
|
+
):
|
|
150
|
+
# Permissions-Policy headers are experimental
|
|
151
|
+
# Clear-Site-Data is too aggressive
|
|
152
|
+
response.headers[header["name"]] = header["value"]
|
|
153
|
+
|
|
154
|
+
if settings.enable_excel_online:
|
|
155
|
+
response.headers["Cross-Origin-Resource-Policy"] = "cross-origin"
|
|
156
|
+
del response.headers["X-Frame-Options"]
|
|
157
|
+
if settings.cdn_officejs:
|
|
158
|
+
response.headers["Cross-Origin-Resource-Policy"] = "cross-origin"
|
|
159
|
+
del response.headers["Cross-Origin-Embedder-Policy"]
|
|
160
|
+
|
|
161
|
+
# Custom headers
|
|
162
|
+
for header_name, header_value in settings.custom_headers.items():
|
|
163
|
+
response.headers[header_name] = header_value
|
|
164
|
+
|
|
165
|
+
return response
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Static files: in prod might be served by something like nginx or via
|
|
169
|
+
# https://github.com/matthiask/blacknoise or https://github.com/Archmonger/ServeStatic
|
|
170
|
+
# Auto-detect dist/ directory for production builds with hashed filenames
|
|
171
|
+
dist_static = PROJECT_DIR / "dist" / "static"
|
|
172
|
+
if dist_static.exists() and settings.environment != "dev":
|
|
173
|
+
app.mount(
|
|
174
|
+
settings.static_url_path,
|
|
175
|
+
StaticFiles(directory=dist_static),
|
|
176
|
+
name="static",
|
|
177
|
+
)
|
|
178
|
+
logger.info("Serving static files from dist/ (production build)")
|
|
179
|
+
else:
|
|
180
|
+
app.mount(
|
|
181
|
+
settings.static_url_path,
|
|
182
|
+
OverridableStaticFiles(
|
|
183
|
+
package_directory=PACKAGE_DIR / "static",
|
|
184
|
+
user_directory=PROJECT_DIR / "static",
|
|
185
|
+
),
|
|
186
|
+
name="static",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if settings.enable_wasm:
|
|
191
|
+
# For xlwings Wasm development
|
|
192
|
+
app.mount(
|
|
193
|
+
# Use the same path prefix as for static files
|
|
194
|
+
settings.static_url_path.replace("static", "wasm"),
|
|
195
|
+
OverridableStaticFiles(
|
|
196
|
+
package_directory=PACKAGE_DIR / "wasm",
|
|
197
|
+
user_directory=PROJECT_DIR / "wasm",
|
|
198
|
+
),
|
|
199
|
+
name="wasm",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
app.mount(
|
|
203
|
+
# Use the same path prefix as for static files
|
|
204
|
+
settings.static_url_path.replace("static", "custom_functions"),
|
|
205
|
+
OverridableStaticFiles(
|
|
206
|
+
package_directory=PACKAGE_DIR / "custom_functions",
|
|
207
|
+
user_directory=PROJECT_DIR / "custom_functions",
|
|
208
|
+
),
|
|
209
|
+
name="custom_functions",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
app.mount(
|
|
213
|
+
# Use the same path prefix as for static files
|
|
214
|
+
settings.static_url_path.replace("static", "custom_scripts"),
|
|
215
|
+
OverridableStaticFiles(
|
|
216
|
+
package_directory=PACKAGE_DIR / "custom_scripts",
|
|
217
|
+
user_directory=PROJECT_DIR / "custom_scripts",
|
|
218
|
+
),
|
|
219
|
+
name="custom_scripts",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if settings.environment == "dev":
|
|
223
|
+
# Don't cache static files
|
|
224
|
+
StaticFiles.is_not_modified = lambda *args, **kwargs: False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Exception handlers
|
|
228
|
+
@app.exception_handler(XlwingsError)
|
|
229
|
+
async def xlwings_exception_handler(request, exception):
|
|
230
|
+
logger.error(exception)
|
|
231
|
+
msg = str(exception)
|
|
232
|
+
return PlainTextResponse(msg, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@app.exception_handler(Exception)
|
|
236
|
+
async def exception_handler(request, exception):
|
|
237
|
+
logger.error(exception)
|
|
238
|
+
if settings.environment == "dev":
|
|
239
|
+
msg = repr(exception)
|
|
240
|
+
else:
|
|
241
|
+
msg = "An error occurred."
|
|
242
|
+
return PlainTextResponse(msg, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Try to import User model from project directory first (user override)
|
|
2
|
+
# Fall back to package location (default implementation)
|
|
3
|
+
try:
|
|
4
|
+
from models.user import User
|
|
5
|
+
except ModuleNotFoundError:
|
|
6
|
+
from .user import User
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CurrentUser(User):
|
|
10
|
+
"""
|
|
11
|
+
Will deliver the current user when used as type hint in a custom function/script
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
pass
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, model_validator
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from typing import Self
|
|
8
|
+
except ImportError:
|
|
9
|
+
# Python < 3.11
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class User(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
User model. This is set up so that it populates most fields from claims
|
|
18
|
+
as returned by OIDC tokens (e.g., Entra ID). You can also use the model by setting
|
|
19
|
+
the attributes directly though.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
id: str | None = None
|
|
23
|
+
name: str | None = None
|
|
24
|
+
domain: str | None = None
|
|
25
|
+
email: str | None = None
|
|
26
|
+
roles: list[str] = []
|
|
27
|
+
ip_address: str | None = None
|
|
28
|
+
claims: dict[str, Any] = Field({}, repr=False)
|
|
29
|
+
|
|
30
|
+
@model_validator(mode="after")
|
|
31
|
+
def populate_from_claims(self) -> Self:
|
|
32
|
+
if self.claims:
|
|
33
|
+
self.id = self.id or self.claims.get("oid")
|
|
34
|
+
self.name = self.name or self.claims.get("name")
|
|
35
|
+
self.email = self.email or self.claims.get("preferred_username")
|
|
36
|
+
self.roles = self.roles or self.claims.get("roles", [])
|
|
37
|
+
if not self.domain and self.email and "@" in self.email:
|
|
38
|
+
self.domain = self.email.split("@")[1]
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
async def has_required_roles(self, required_roles: list[str] | None = None):
|
|
42
|
+
if required_roles:
|
|
43
|
+
if set(required_roles).issubset(self.roles):
|
|
44
|
+
logger.info(f"User has required roles: {self.name}")
|
|
45
|
+
return True
|
|
46
|
+
else:
|
|
47
|
+
return False
|
|
48
|
+
else:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
async def is_authorized(self):
|
|
52
|
+
"""Method that can be overridden to implement a global authorization logic"""
|
|
53
|
+
return True
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import zlib
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import numpy as np
|
|
6
|
+
except ImportError:
|
|
7
|
+
np = None
|
|
8
|
+
try:
|
|
9
|
+
import pandas as pd
|
|
10
|
+
except ImportError:
|
|
11
|
+
pd = None
|
|
12
|
+
import redis
|
|
13
|
+
from croniter import croniter
|
|
14
|
+
from xlwings import XlwingsError
|
|
15
|
+
from xlwings.constants import ObjectHandleIcons
|
|
16
|
+
from xlwings.conversion import Converter
|
|
17
|
+
|
|
18
|
+
from .config import settings
|
|
19
|
+
from .routers import xlwings as xlwings_router
|
|
20
|
+
from .serializers import deserialize, serialize
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Used if XLWINGS_OBJECT_CACHE_URL, i.e., Redis isn't configured.
|
|
25
|
+
# Only useful with a single worker e.g., during development.
|
|
26
|
+
cache = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ObjectCacheConverter(Converter):
|
|
30
|
+
@staticmethod
|
|
31
|
+
def read_value(cell_address, options):
|
|
32
|
+
# For custom function args of type Entity, the frontend sends the cell address
|
|
33
|
+
# instead of the cell value
|
|
34
|
+
redis_client: redis.Redis = xlwings_router.redis_client_context.get()
|
|
35
|
+
if settings.object_cache_url and not redis_client:
|
|
36
|
+
raise XlwingsError("Failed to connect to Redis")
|
|
37
|
+
key = f"object:{cell_address}"
|
|
38
|
+
if settings.object_cache_url:
|
|
39
|
+
value = redis_client.get(key)
|
|
40
|
+
if not value:
|
|
41
|
+
raise XlwingsError("Object cache is empty")
|
|
42
|
+
if settings.object_cache_enable_compression:
|
|
43
|
+
value = zlib.decompress(value).decode()
|
|
44
|
+
else:
|
|
45
|
+
value = value.decode()
|
|
46
|
+
else:
|
|
47
|
+
value = cache.get(key)
|
|
48
|
+
if not value:
|
|
49
|
+
raise XlwingsError("Object cache is empty")
|
|
50
|
+
obj = deserialize(value)
|
|
51
|
+
return obj
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def write_value(obj, options):
|
|
55
|
+
redis_client: redis.Redis = xlwings_router.redis_client_context.get()
|
|
56
|
+
if settings.object_cache_url and not redis_client:
|
|
57
|
+
raise XlwingsError("Failed to connect to Redis")
|
|
58
|
+
key = f"object:{xlwings_router.caller_address_context.get()}"
|
|
59
|
+
values = serialize(obj)
|
|
60
|
+
if settings.object_cache_url:
|
|
61
|
+
expire_at = None
|
|
62
|
+
if settings.object_cache_expire_at:
|
|
63
|
+
cron = croniter(settings.object_cache_expire_at)
|
|
64
|
+
expire_at = int(cron.get_next())
|
|
65
|
+
if settings.object_cache_enable_compression:
|
|
66
|
+
values = zlib.compress(values.encode())
|
|
67
|
+
redis_client.set(key, values, exat=expire_at)
|
|
68
|
+
else:
|
|
69
|
+
logger.warning(
|
|
70
|
+
"Storing objects in memory. Configure XLWINGS_OBJECT_CACHE_URL "
|
|
71
|
+
"for production use!"
|
|
72
|
+
)
|
|
73
|
+
cache[key] = values
|
|
74
|
+
|
|
75
|
+
obj_type = type(obj).__name__
|
|
76
|
+
|
|
77
|
+
result = {
|
|
78
|
+
"type": "Entity",
|
|
79
|
+
"text": options.get("text", obj_type) or obj_type,
|
|
80
|
+
"properties": {
|
|
81
|
+
"Type": {
|
|
82
|
+
"type": "String",
|
|
83
|
+
"basicValue": obj_type,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
"layouts": {
|
|
87
|
+
"compact": {
|
|
88
|
+
"icon": options.get("icon", ObjectHandleIcons.generic)
|
|
89
|
+
or ObjectHandleIcons.generic
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Shape
|
|
95
|
+
def get_shape(obj):
|
|
96
|
+
if pd and isinstance(obj, pd.DataFrame):
|
|
97
|
+
return f"{obj.shape}"
|
|
98
|
+
if np and isinstance(obj, np.ndarray):
|
|
99
|
+
return f"{obj.shape}"
|
|
100
|
+
elif isinstance(obj, (list, tuple)):
|
|
101
|
+
if obj and isinstance(obj[0], (list, tuple)):
|
|
102
|
+
return f"({len(obj)}, {len(obj[0])})"
|
|
103
|
+
return f"({len(obj)},)"
|
|
104
|
+
else:
|
|
105
|
+
try:
|
|
106
|
+
return f"{len(obj)} (length)"
|
|
107
|
+
except Exception:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
shape_value = get_shape(obj)
|
|
111
|
+
if shape_value:
|
|
112
|
+
result["properties"]["Shape"] = {
|
|
113
|
+
"type": "String",
|
|
114
|
+
"basicValue": shape_value,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Columns
|
|
118
|
+
cols_info = None
|
|
119
|
+
if pd and isinstance(obj, pd.DataFrame):
|
|
120
|
+
cols_info = ", ".join(f"{col} [{obj[col].dtype}]" for col in obj.columns)
|
|
121
|
+
if cols_info:
|
|
122
|
+
result["properties"]["Columns"] = {
|
|
123
|
+
"type": "String",
|
|
124
|
+
"basicValue": cols_info,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Index
|
|
128
|
+
index_info = None
|
|
129
|
+
if pd and isinstance(obj, pd.DataFrame):
|
|
130
|
+
index_type = type(obj.index).__name__
|
|
131
|
+
index_length = len(obj.index)
|
|
132
|
+
index_start = obj.index[0]
|
|
133
|
+
index_end = obj.index[-1]
|
|
134
|
+
index_info = (
|
|
135
|
+
f"{index_type}: {index_length} entries, {index_start} to {index_end}"
|
|
136
|
+
)
|
|
137
|
+
if index_info:
|
|
138
|
+
result["properties"]["Index"] = {
|
|
139
|
+
"type": "String",
|
|
140
|
+
"basicValue": index_info,
|
|
141
|
+
}
|
|
142
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Header, Request
|
|
6
|
+
|
|
7
|
+
from ..config import settings
|
|
8
|
+
from ..templates import TemplateResponse
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix=settings.app_path)
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_manifest_response(
|
|
16
|
+
request: Request, protocol: str, download: bool = False
|
|
17
|
+
) -> TemplateResponse:
|
|
18
|
+
if settings.hostname:
|
|
19
|
+
# Settings
|
|
20
|
+
base_url = f"https://{settings.hostname}"
|
|
21
|
+
elif os.getenv("RENDER_EXTERNAL_URL"):
|
|
22
|
+
# Render.com
|
|
23
|
+
base_url = os.getenv("RENDER_EXTERNAL_URL")
|
|
24
|
+
elif os.getenv("WEBSITE_HOSTNAME"):
|
|
25
|
+
# Azure Functions and Azure App Service
|
|
26
|
+
base_url = f"https://{os.getenv('WEBSITE_HOSTNAME')}"
|
|
27
|
+
elif os.getenv("CODESPACES"):
|
|
28
|
+
# GitHub Codespaces
|
|
29
|
+
base_url = f"https://{os.getenv('CODESPACE_NAME')}-8000.{os.getenv('GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN')}"
|
|
30
|
+
else:
|
|
31
|
+
base_url = str(request.base_url)
|
|
32
|
+
if protocol == "https":
|
|
33
|
+
base_url = base_url.replace("http://", "https://")
|
|
34
|
+
|
|
35
|
+
base_url = str(base_url).rstrip("/")
|
|
36
|
+
|
|
37
|
+
manifest_ids = {
|
|
38
|
+
"dev": settings.manifest_id_dev,
|
|
39
|
+
"qa": settings.manifest_id_qa,
|
|
40
|
+
"uat": settings.manifest_id_uat,
|
|
41
|
+
"staging": settings.manifest_id_staging,
|
|
42
|
+
"prod": settings.manifest_id_prod,
|
|
43
|
+
}
|
|
44
|
+
manifest_id = manifest_ids[settings.environment]
|
|
45
|
+
|
|
46
|
+
headers = {}
|
|
47
|
+
if download:
|
|
48
|
+
sanitized_name = re.sub(
|
|
49
|
+
r"[^a-z0-9]+", "-", settings.project_name.lower()
|
|
50
|
+
).strip("-")
|
|
51
|
+
filename = f"{sanitized_name}-{settings.environment}.xml"
|
|
52
|
+
headers["Content-Disposition"] = f"attachment; filename={filename}"
|
|
53
|
+
|
|
54
|
+
return TemplateResponse(
|
|
55
|
+
request=request,
|
|
56
|
+
name="manifest.xml",
|
|
57
|
+
context={
|
|
58
|
+
"settings": settings,
|
|
59
|
+
"base_url": base_url,
|
|
60
|
+
"base_url_with_app_path": f"{base_url}{settings.app_path}",
|
|
61
|
+
"manifest_id": manifest_id,
|
|
62
|
+
},
|
|
63
|
+
media_type="text/plain",
|
|
64
|
+
headers=headers,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.get("/manifest")
|
|
69
|
+
@router.get("/manifest.xml")
|
|
70
|
+
async def manifest(
|
|
71
|
+
request: Request,
|
|
72
|
+
protocol: str = Header(default="", alias="X-Forwarded-Proto"),
|
|
73
|
+
):
|
|
74
|
+
return _get_manifest_response(request, protocol, download=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get("/manifest/download")
|
|
78
|
+
async def manifest_download(
|
|
79
|
+
request: Request,
|
|
80
|
+
protocol: str = Header(default="", alias="X-Forwarded-Proto"),
|
|
81
|
+
):
|
|
82
|
+
return _get_manifest_response(request, protocol, download=True)
|