cmdbox 0.6.4.2__py3-none-any.whl → 0.6.6__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.
Potentially problematic release.
This version of cmdbox might be problematic. Click here for more details.
- cmdbox/app/app.py +7 -0
- cmdbox/app/client.py +384 -383
- cmdbox/app/common.py +85 -7
- cmdbox/app/commons/convert.py +3 -1
- cmdbox/app/edge.py +12 -12
- cmdbox/app/feature.py +1 -1
- cmdbox/app/features/cli/{cmdbox_vision_install.py → _cmdbox_vision_install.py} +2 -10
- cmdbox/app/features/cli/_cmdbox_vision_predict.py +487 -0
- cmdbox/app/features/cli/{cmdbox_vision_start.py → _cmdbox_vision_start.py} +5 -1
- cmdbox/app/features/cli/cmdbox_audit_createdb.py +1 -11
- cmdbox/app/features/cli/cmdbox_audit_delete.py +0 -9
- cmdbox/app/features/cli/cmdbox_audit_search.py +0 -9
- cmdbox/app/features/cli/cmdbox_audit_write.py +0 -9
- cmdbox/app/features/cli/cmdbox_cmd_list.py +3 -3
- cmdbox/app/features/cli/cmdbox_cmd_load.py +3 -3
- cmdbox/app/features/cli/cmdbox_excel_cell_details.py +436 -0
- cmdbox/app/features/cli/cmdbox_excel_cell_search.py +276 -0
- cmdbox/app/features/cli/cmdbox_excel_cell_values.py +258 -0
- cmdbox/app/features/cli/cmdbox_excel_sheet_list.py +159 -0
- cmdbox/app/features/cli/cmdbox_tts_install.py +4 -11
- cmdbox/app/features/cli/cmdbox_tts_say.py +2 -10
- cmdbox/app/features/cli/cmdbox_tts_start.py +0 -9
- cmdbox/app/features/cli/cmdbox_tts_stop.py +0 -9
- cmdbox/app/features/cli/cmdbox_web_apikey_add.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_apikey_del.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_group_add.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_group_del.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_group_edit.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_group_list.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_start.py +10 -10
- cmdbox/app/features/cli/cmdbox_web_user_add.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_user_del.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_user_edit.py +3 -3
- cmdbox/app/features/cli/cmdbox_web_user_list.py +3 -3
- cmdbox/app/features/cli/excel_base.py +301 -0
- cmdbox/app/features/web/cmdbox_web_exec_cmd.py +12 -14
- cmdbox/app/filer.py +5 -2
- cmdbox/app/mcp.py +4 -3
- cmdbox/app/options.py +8 -0
- cmdbox/app/web.py +58 -39
- cmdbox/extensions/features.yml +3 -0
- cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +0 -9
- cmdbox/licenses/LICENSE_Mako_1_3_10_MIT_License.txt +19 -0
- cmdbox/licenses/LICENSE_alembic_1_16_5_UNKNOWN.txt +19 -0
- cmdbox/licenses/{LICENSE_cffi_1_17_1_MIT_License.txt → LICENSE_cffi_2_0_0_UNKNOWN.txt} +2 -5
- cmdbox/licenses/LICENSE_debugpy_1_8_17_MIT_License.txt +24 -0
- cmdbox/licenses/LICENSE_et_xmlfile_2_0_0_MIT_License.txt +298 -0
- cmdbox/licenses/LICENSE_fastuuid_0_13_5_BSD_License.txt +29 -0
- cmdbox/licenses/LICENSE_google-cloud-monitoring_2_27_2_Apache_Software_License.txt +202 -0
- cmdbox/licenses/LICENSE_google-cloud-spanner_3_58_0_Apache_Software_License.txt +202 -0
- cmdbox/licenses/LICENSE_google-genai_1_40_0_Apache_Software_License.txt +202 -0
- cmdbox/licenses/LICENSE_grpc-interceptor_0_15_4_MIT_License.txt +21 -0
- cmdbox/licenses/{LICENSE_lazy-object-proxy_1_11_0_BSD_License.txt → LICENSE_lazy-object-proxy_1_12_0_UNKNOWN.txt} +1 -1
- cmdbox/licenses/LICENSE_openpyxl_3_1_5_MIT_License.txt +23 -0
- cmdbox/licenses/LICENSE_opentelemetry-exporter-otlp-proto-common_1_37_0_UNKNOWN.txt +201 -0
- cmdbox/licenses/LICENSE_opentelemetry-exporter-otlp-proto-http_1_37_0_UNKNOWN.txt +201 -0
- cmdbox/licenses/LICENSE_opentelemetry-proto_1_37_0_UNKNOWN.txt +201 -0
- cmdbox/licenses/LICENSE_opentelemetry-sdk_1_37_0_UNKNOWN.txt +201 -0
- cmdbox/licenses/LICENSE_opentelemetry-semantic-conventions_0_58b0_UNKNOWN.txt +201 -0
- cmdbox/licenses/LICENSE_sqlalchemy-spanner_1_16_0_Apache_Software_License.txt +202 -0
- cmdbox/licenses/LICENSE_sqlparse_0_5_3_BSD_License.txt +25 -0
- cmdbox/licenses/{LICENSE_uvicorn_0_35_0_BSD_License.txt → LICENSE_uvicorn_0_37_0_BSD_License.txt} +2 -1
- cmdbox/licenses/files.txt +82 -71
- cmdbox/version.py +2 -2
- cmdbox/web/assets/cmdbox/svgicon.js +9 -0
- {cmdbox-0.6.4.2.dist-info → cmdbox-0.6.6.dist-info}/METADATA +29 -29
- {cmdbox-0.6.4.2.dist-info → cmdbox-0.6.6.dist-info}/RECORD +133 -117
- cmdbox/app/features/cli/cmdbox_vision_predict.py +0 -192
- cmdbox/licenses/LICENSE_APScheduler_3_11_0_MIT_License.txt +0 -19
- cmdbox/licenses/LICENSE_backoff_2_2_1_MIT_License.txt +0 -21
- cmdbox/licenses/LICENSE_fastapi-sso_0_18_0_MIT_License.txt +0 -21
- cmdbox/licenses/LICENSE_litellm-enterprise_0_1_19_UNKNOWN.txt +0 -37
- cmdbox/licenses/LICENSE_oauthlib_3_3_1_BSD-3-Clause.txt +0 -27
- cmdbox/licenses/LICENSE_orjson_3_11_1_Apache_Software_License-MIT_License.txt +0 -201
- /cmdbox/licenses/{LICENSE_Authlib_1_6_1_BSD_License.txt → LICENSE_Authlib_1_6_5_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_MarkupSafe_3_0_2_BSD_License.txt → LICENSE_MarkupSafe_3_0_3_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_PyYAML_6_0_2_MIT_License.txt → LICENSE_PyYAML_6_0_3_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_anyio_4_10_0_UNKNOWN.txt → LICENSE_anyio_4_11_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_cachetools_5_5_2_MIT_License.txt → LICENSE_cachetools_6_2_0_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_click_8_2_1_UNKNOWN.txt → LICENSE_click_8_3_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_cryptography_45_0_6_Apache-2_0_OR_BSD-3-Clause.txt → LICENSE_cryptography_46_0_2_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_cyclopts_3_22_5_Apache_Software_License.txt → LICENSE_cyclopts_3_24_0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_dnspython_2_7_0_ISC_License-ISCL.txt → LICENSE_dnspython_2_8_0_ISC_License-ISCL.txt} +0 -0
- /cmdbox/licenses/{LICENSE_email_validator_2_2_0_The_Unlicense-Unlicense.txt → LICENSE_email-validator_2_3_0_The_Unlicense-Unlicense.txt} +0 -0
- /cmdbox/licenses/{LICENSE_fastapi_0_116_1_MIT_License.txt → LICENSE_fastapi_0_118_0_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_fastmcp_2_11_3_Apache_Software_License.txt → LICENSE_fastmcp_2_12_4_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_filelock_3_18_0_The_Unlicense-Unlicense.txt → LICENSE_filelock_3_19_1_The_Unlicense-Unlicense.txt} +0 -0
- /cmdbox/licenses/{LICENSE_fsspec_2025_7_0_BSD_License.txt → LICENSE_fsspec_2025_9_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_gevent_25_5_1_MIT.txt → LICENSE_gevent_25_9_1_MIT.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-adk_1_10_0_Apache_Software_License.txt → LICENSE_google-adk_1_15_1_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-api-core_2_25_1_Apache_Software_License.txt → LICENSE_google-api-core_2_25_2_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-api-python-client_2_178_0_Apache_Software_License.txt → LICENSE_google-api-python-client_2_184_0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-auth_2_40_3_Apache_Software_License.txt → LICENSE_google-auth_2_41_1_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-cloud-aiplatform_1_108_0_Apache_2_0.txt → LICENSE_google-cloud-aiplatform_1_119_0_Apache_2_0.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-cloud-bigquery_3_35_1_Apache_Software_License.txt → LICENSE_google-cloud-bigquery_3_38_0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_google-genai_1_29_0_Apache_Software_License.txt → LICENSE_google-cloud-bigtable_2_32_0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_grpcio-status_1_74_0_Apache_Software_License.txt → LICENSE_grpcio-status_1_75_1_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_grpcio_1_74_0_Apache_Software_License.txt → LICENSE_grpcio_1_75_1_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_httplib2_0_22_0_MIT_License.txt → LICENSE_httplib2_0_31_0_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_huggingface-hub_0_34_4_Apache_Software_License.txt → LICENSE_huggingface-hub_0_35_3_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_jaraco_functools_4_2_1_UNKNOWN.txt → LICENSE_jaraco_functools_4_3_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_jiter_0_10_0_MIT_License.txt → LICENSE_jiter_0_11_0_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_jsonschema-specifications_2025_4_1_UNKNOWN.txt → LICENSE_jsonschema-specifications_2025_9_1_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_jsonschema_4_25_0_UNKNOWN.txt → LICENSE_jsonschema_4_25_1_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_litellm_1_75_5_post1_MIT_License.txt → LICENSE_litellm_1_77_5_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_mcp_1_12_4_MIT_License.txt → LICENSE_mcp_1_16_0_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_more-itertools_10_7_0_MIT_License.txt → LICENSE_more-itertools_10_8_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_numpy_2_3_2_BSD_License.txt → LICENSE_numpy_2_3_3_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_openai_1_99_9_Apache_Software_License.txt → LICENSE_openai_2_1_0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_opentelemetry-api_1_36_0_UNKNOWN.txt → LICENSE_opentelemetry-api_1_37_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_opentelemetry-sdk_1_36_0_UNKNOWN.txt → LICENSE_opentelemetry-exporter-gcp-logging_1_9_0a0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_opentelemetry-semantic-conventions_0_57b0_UNKNOWN.txt → LICENSE_opentelemetry-exporter-gcp-monitoring_1_9_0a0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_prompt_toolkit_3_0_51_BSD_License.txt → LICENSE_prompt_toolkit_3_0_52_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_protobuf_6_31_1_3-Clause_BSD_License.txt → LICENSE_protobuf_6_32_1_3-Clause_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_psycopg-binary_3_2_9_GNU_Lesser_General_Public_License_v3-LGPLv3.txt → LICENSE_psycopg-binary_3_2_10_GNU_Lesser_General_Public_License_v3-LGPLv3.txt} +0 -0
- /cmdbox/licenses/{LICENSE_psycopg_3_2_9_GNU_Lesser_General_Public_License_v3-LGPLv3.txt → LICENSE_psycopg_3_2_10_GNU_Lesser_General_Public_License_v3-LGPLv3.txt} +0 -0
- /cmdbox/licenses/{LICENSE_pycparser_2_22_BSD_License.txt → LICENSE_pycparser_2_23_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_pydantic-settings_2_10_1_MIT_License.txt → LICENSE_pydantic-settings_2_11_0_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_pydantic_2_11_7_MIT_License.txt → LICENSE_pydantic_2_11_10_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_pyparsing_3_2_3_MIT_License.txt → LICENSE_pyparsing_3_2_5_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_pyperclip_1_9_0_BSD_License.txt → LICENSE_pyperclip_1_11_0_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_questionary_2_1_0_MIT_License.txt → LICENSE_questionary_2_1_1_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_regex_2025_7_34_UNKNOWN.txt → LICENSE_regex_2025_9_18_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_requests_2_32_4_Apache_Software_License.txt → LICENSE_requests_2_32_5_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_rpds-py_0_27_0_UNKNOWN.txt → LICENSE_rpds-py_0_27_1_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_shapely_2_1_1_BSD_License.txt → LICENSE_shapely_2_1_2_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_sphinx-sitemap_2_7_2_UNKNOWN.txt → LICENSE_sphinx-sitemap_2_8_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_starlette_0_47_2_BSD_License.txt → LICENSE_starlette_0_48_0_BSD_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_tenacity_9_1_2_Apache_Software_License.txt → LICENSE_tenacity_8_5_0_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_tokenizers_0_21_4_Apache_Software_License.txt → LICENSE_tokenizers_0_22_1_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_twine_6_1_0_Apache_Software_License.txt → LICENSE_twine_6_2_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_typing-inspection_0_4_1_UNKNOWN.txt → LICENSE_typing-inspection_0_4_2_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_typing_extensions_4_14_1_UNKNOWN.txt → LICENSE_typing_extensions_4_15_0_UNKNOWN.txt} +0 -0
- /cmdbox/licenses/{LICENSE_wcwidth_0_2_13_MIT_License.txt → LICENSE_wcwidth_0_2_14_MIT_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_zope_event_5_1_1_Zope_Public_License.txt → LICENSE_zope_event_6_0_Zope_Public_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_zope_interface_7_2_Zope_Public_License.txt → LICENSE_zope_interface_8_0_1_Zope_Public_License.txt} +0 -0
- {cmdbox-0.6.4.2.dist-info → cmdbox-0.6.6.dist-info}/WHEEL +0 -0
- {cmdbox-0.6.4.2.dist-info → cmdbox-0.6.6.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.6.4.2.dist-info → cmdbox-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {cmdbox-0.6.4.2.dist-info → cmdbox-0.6.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from cmdbox.app import common, filer
|
|
2
|
+
from cmdbox.app.commons import convert, redis_client
|
|
3
|
+
from cmdbox.app.features.cli import excel_base
|
|
4
|
+
from cmdbox.app.options import Options
|
|
5
|
+
from datetime import datetime, timedelta, time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, List, Tuple
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExcelCellDetails(excel_base.ExcelBase):
|
|
14
|
+
def get_cmd(self):
|
|
15
|
+
"""
|
|
16
|
+
この機能のコマンドを返します
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: コマンド
|
|
20
|
+
"""
|
|
21
|
+
return 'cell_details'
|
|
22
|
+
|
|
23
|
+
def get_option(self):
|
|
24
|
+
"""
|
|
25
|
+
この機能のオプションを返します
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict[str, Any]: オプション
|
|
29
|
+
"""
|
|
30
|
+
opt = super().get_option()
|
|
31
|
+
opt['description_ja'] = "データフォルダ配下のExcelファイルの指定したセルの詳細情報を取得します。"
|
|
32
|
+
opt['description_en'] = "Get the details of the specified cell in the Excel file under the data folder."
|
|
33
|
+
opt['choice'] += [
|
|
34
|
+
dict(opt="formula_data_only", type=Options.T_BOOL, default=False, required=True, multi=False, hide=False, choice=[True, False],
|
|
35
|
+
description_ja="数式データのみを参照するかどうかを指定します。このオプションはキャッシュされたデータが存在する場合に有効です。",
|
|
36
|
+
description_en="Specify whether to get only formula data. This option is valid if cached data exists."),
|
|
37
|
+
dict(opt="sheet_name", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
38
|
+
description_ja="セルの値を取得するシートの名前を指定します。省略した場合、最初のシートが使用されます。",
|
|
39
|
+
description_en="Specify the sheet name to get the cell value.If omitted, the first sheet will be used."),
|
|
40
|
+
dict(opt="cell_name", type=Options.T_STR, default=None, required=False, multi=True, hide=False, choice=None,
|
|
41
|
+
description_ja="セルの値を取得するセルの名前を指定します。例えば、`A1`、`B2`、`R5987`。",
|
|
42
|
+
description_en="Specify the cell name to get the cell value. For example, `A1`, `B2`, `R5987`."),
|
|
43
|
+
dict(opt="cell_top_left", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
44
|
+
description_ja="セルの値を取得する左上セルの名前を指定します。例えば、`A1`、`B2`、`R5987`。",
|
|
45
|
+
description_en="Specify the top-left cell name to get the cell value. For example, `A1`, `B2`, `R5987`."),
|
|
46
|
+
dict(opt="cell_bottom_right", type=Options.T_STR, default=None, required=False, multi=False, hide=False, choice=None,
|
|
47
|
+
description_ja="セルの値を取得する右下セルの名前を指定します。例えば、`A1`、`B2`、`R5987`。",
|
|
48
|
+
description_en="Specify the bottom-right cell name to get the cell value. For example, `A1`, `B2`, `R5987`."),
|
|
49
|
+
dict(opt="output_detail_format", type=Options.T_STR, default='json', required=False, multi=False, hide=False, choice=['json', 'text'],
|
|
50
|
+
description_ja="出力フォーマットを指定します。例えば、`json`、`text`。",
|
|
51
|
+
description_en="Specify the output format. For example, `json`, `text`."),
|
|
52
|
+
]
|
|
53
|
+
return opt
|
|
54
|
+
|
|
55
|
+
def chk_args(self, args:argparse.Namespace, tm:float, pf:List[Dict[str, float]]=[]) -> Tuple[bool, str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
引数のチェックを行います
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
args (argparse.Namespace): 引数
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple[bool, str]: チェック結果, メッセージ
|
|
64
|
+
"""
|
|
65
|
+
if args.svname is None:
|
|
66
|
+
msg = dict(warn=f"Please specify the --svname option.")
|
|
67
|
+
common.print_format(msg, args.format, tm, args.output_json, args.output_json_append, pf=pf)
|
|
68
|
+
return self.RESP_WARN, msg, None
|
|
69
|
+
if args.scope is None:
|
|
70
|
+
msg = dict(warn=f"Please specify the --scope option.")
|
|
71
|
+
common.print_format(msg, args.format, tm, args.output_json, args.output_json_append, pf=pf)
|
|
72
|
+
return self.RESP_WARN, msg, None
|
|
73
|
+
if args.svpath is None:
|
|
74
|
+
msg = dict(warn=f"Please specify the --svpath option.")
|
|
75
|
+
common.print_format(msg, args.format, tm, args.output_json, args.output_json_append, pf=pf)
|
|
76
|
+
return self.RESP_WARN, msg, None
|
|
77
|
+
return self.RESP_SUCCESS, None, None
|
|
78
|
+
|
|
79
|
+
def excel_proc(self, abspath:Path, args:argparse.Namespace, logger:logging.Logger, tm:float, pf:List[Dict[str, float]]=[]) -> Dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
Excel処理のベース
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
abspath (Path): Excelファイルの絶対パス
|
|
85
|
+
args (argparse.Namespace): 引数
|
|
86
|
+
logger (logging.Logger): ロガー
|
|
87
|
+
tm (float): 処理時間
|
|
88
|
+
pf (List[Dict[str, float]]): パフォーマンス情報
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict[str, Any]: 結果
|
|
92
|
+
"""
|
|
93
|
+
res = self.get_cell_details(abspath, args.formula_data_only, args.sheet_name,
|
|
94
|
+
args.cell_name, args.cell_top_left, args.cell_bottom_right,
|
|
95
|
+
args.output_detail_format, logger)
|
|
96
|
+
return res
|
|
97
|
+
|
|
98
|
+
def get_svparam(self, args:argparse.Namespace) -> List[str]:
|
|
99
|
+
"""
|
|
100
|
+
サーバーに送信するパラメーターを返します
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
args (argparse.Namespace): 引数
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List[str]: サーバーに送信するパラメーター
|
|
107
|
+
"""
|
|
108
|
+
cell_name = json.dumps(args.cell_name, default=common.default_json_enc) if args.cell_name is not None else '[]'
|
|
109
|
+
ret = [convert.str2b64str(str(args.svpath)), str(args.formula_data_only), convert.str2b64str(str(args.sheet_name)),
|
|
110
|
+
convert.str2b64str(cell_name), convert.str2b64str(args.cell_top_left), convert.str2b64str(args.cell_bottom_right),
|
|
111
|
+
convert.str2b64str(args.output_detail_format)]
|
|
112
|
+
return ret
|
|
113
|
+
|
|
114
|
+
def is_cluster_redirect(self):
|
|
115
|
+
"""
|
|
116
|
+
クラスター宛のメッセージの場合、メッセージを転送するかどうかを返します
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
bool: メッセージを転送する場合はTrue
|
|
120
|
+
"""
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def svrun(self, data_dir:Path, logger:logging.Logger, redis_cli:redis_client.RedisClient, msg:List[str],
|
|
124
|
+
sessions:Dict[str, Dict[str, Any]]) -> int:
|
|
125
|
+
"""
|
|
126
|
+
この機能のサーバー側の実行を行います
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
data_dir (Path): データディレクトリ
|
|
130
|
+
logger (logging.Logger): ロガー
|
|
131
|
+
redis_cli (redis_client.RedisClient): Redisクライアント
|
|
132
|
+
msg (List[str]): 受信メッセージ
|
|
133
|
+
sessions (Dict[str, Dict[str, Any]]): セッション情報
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
int: 終了コード
|
|
137
|
+
"""
|
|
138
|
+
svpath = convert.b64str2str(msg[2])
|
|
139
|
+
formula_data_only = msg[3]=='True'
|
|
140
|
+
sheet_name = convert.b64str2str(msg[4])
|
|
141
|
+
sheet_name = None if sheet_name=='None' else sheet_name
|
|
142
|
+
cell_name = json.loads(convert.b64str2str(msg[5]))
|
|
143
|
+
cell_top_left = convert.b64str2str(msg[6])
|
|
144
|
+
cell_bottom_right = convert.b64str2str(msg[7])
|
|
145
|
+
output_detail_format = convert.b64str2str(msg[8])
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
f = filer.Filer(data_dir, logger)
|
|
149
|
+
chk, abspath, res = f._file_exists(svpath)
|
|
150
|
+
if not chk:
|
|
151
|
+
logger.warning(f"File not found. {svpath}")
|
|
152
|
+
redis_cli.rpush(msg[1], res)
|
|
153
|
+
return self.RESP_WARN
|
|
154
|
+
res = self.get_cell_details(abspath, formula_data_only, sheet_name, cell_name, cell_top_left, cell_bottom_right,
|
|
155
|
+
output_detail_format, logger)
|
|
156
|
+
redis_cli.rpush(msg[1], res)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.warning(f"Failed to cell details: {e}", exc_info=True)
|
|
159
|
+
redis_cli.rpush(msg[1], dict(warn=f"Failed to cell details: {e}"))
|
|
160
|
+
return self.RESP_WARN
|
|
161
|
+
return self.RESP_SUCCESS
|
|
162
|
+
|
|
163
|
+
def get_cell_details(self, filepath:str, formula_data_only:bool, sheet_name:str, cell_name:List[str], cell_top_left:str, cell_bottom_right:str,
|
|
164
|
+
output_detail_format:str, logger:logging.Logger) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
指定したワークブックの単一セルの値、データ型、スタイル、コメント、数式、ハイパーリンクなどの詳細を取得します。
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
filepath (str): ワークブックのパス
|
|
170
|
+
formula_data_only (bool): 数式データのみを参照するかどうか。このオプションはキャッシュされたデータが存在する場合に有効です。
|
|
171
|
+
sheet_name (str): 詳細情報を取得するシートの名前
|
|
172
|
+
cell_name (List[str]): 詳細情報を取得するセルの名前のリスト。例えば、`A1`、`B2`、`R5987`。
|
|
173
|
+
cell_top_left (str): 詳細情報を取得する範囲の左上セルの名前。例えば、`A1`、`B2`、`R5987`。
|
|
174
|
+
cell_bottom_right (str): 詳細情報を取得する範囲の右下セルの名前。例えば、`A1`、`B2`、`R5987`。
|
|
175
|
+
output_detail_format (str): 出力フォーマット。`json`または`text`。
|
|
176
|
+
logger (logging.Logger): ロガー
|
|
177
|
+
Returns:
|
|
178
|
+
dict: セルの詳細情報
|
|
179
|
+
"""
|
|
180
|
+
wb:Workbook = None
|
|
181
|
+
try:
|
|
182
|
+
from openpyxl.cell import Cell
|
|
183
|
+
from openpyxl.workbook.workbook import Workbook
|
|
184
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
185
|
+
from openpyxl.utils.datetime import from_excel
|
|
186
|
+
import openpyxl
|
|
187
|
+
|
|
188
|
+
wb:Workbook = openpyxl.load_workbook(filename=filepath, read_only=True, data_only=formula_data_only)
|
|
189
|
+
if sheet_name not in wb.sheetnames:
|
|
190
|
+
if len(wb.sheetnames) <= 0:
|
|
191
|
+
msg = dict(warn=f"There is no worksheet. filepath: {filepath}")
|
|
192
|
+
logger.warning(f"There is no worksheet. filepath: {filepath}")
|
|
193
|
+
return msg
|
|
194
|
+
sheet_name = wb.sheetnames[0]
|
|
195
|
+
|
|
196
|
+
def _proc(cellinfos:Dict[str,Any], sheet:Worksheet, cn:str):
|
|
197
|
+
cell:Cell = sheet[cn]
|
|
198
|
+
celltxt = f"Cell: {cn}\n" \
|
|
199
|
+
+ f" Value: {cell.value}\n" \
|
|
200
|
+
+ f" Data type: {self.OPENPYXL_TYPE_TO_STRING.get(cell.data_type, cell.data_type)}\n" \
|
|
201
|
+
+ f" Number format: {cell.number_format}\n"
|
|
202
|
+
cellinfo = {
|
|
203
|
+
"Cell": cn,
|
|
204
|
+
"Value": cell.value,
|
|
205
|
+
"Data_type": self.OPENPYXL_TYPE_TO_STRING.get(cell.data_type, cell.data_type),
|
|
206
|
+
"Number_format": cell.number_format,
|
|
207
|
+
}
|
|
208
|
+
cellinfos[cn] = cellinfo
|
|
209
|
+
|
|
210
|
+
if hasattr(cell, "style"):
|
|
211
|
+
cellinfo["Style"] = cell.style
|
|
212
|
+
celltxt += f" Style: {cell.style}\n"
|
|
213
|
+
|
|
214
|
+
# 日時データ
|
|
215
|
+
if cell.is_date:
|
|
216
|
+
date_value: datetime | timedelta | time | None = from_excel(cell.value)
|
|
217
|
+
if isinstance(date_value, (datetime, time)):
|
|
218
|
+
cellinfo["Value_as_Date"] = date_value.isoformat()
|
|
219
|
+
celltxt += f" Value as Date: {date_value.isoformat()}\n"
|
|
220
|
+
elif isinstance(date_value, timedelta):
|
|
221
|
+
cellinfo["Value_as_Time_Interval"] = date_value.total_seconds()
|
|
222
|
+
celltxt += f" Value as Time Interval: {date_value.total_seconds()}\n"
|
|
223
|
+
|
|
224
|
+
# 数式
|
|
225
|
+
if cell.data_type == "f":
|
|
226
|
+
cellinfo["Formula"] = cell.value
|
|
227
|
+
celltxt += f" Formula: {cell.value}\n"
|
|
228
|
+
|
|
229
|
+
# ハイパーリンク
|
|
230
|
+
if hasattr(cell, "hyperlink") and cell.hyperlink:
|
|
231
|
+
cellinfo["Hyperlink_Text"] = cell.hyperlink
|
|
232
|
+
celltxt += f" Hyperlink: {cell.hyperlink}\n"
|
|
233
|
+
if hasattr(cell.hyperlink, "target"):
|
|
234
|
+
cellinfo["Hyperlink_Target"] = cell.hyperlink.target
|
|
235
|
+
celltxt += f" Hyperlink Target: {cell.hyperlink.target}\n"
|
|
236
|
+
if hasattr(cell.hyperlink, "tooltip"):
|
|
237
|
+
cellinfo["Hyperlink_Tooltip"] = cell.hyperlink.tooltip
|
|
238
|
+
celltxt += f" Hyperlink Tooltip: {cell.hyperlink.tooltip}\n"
|
|
239
|
+
|
|
240
|
+
# コメント
|
|
241
|
+
if hasattr(cell, "comment") and cell.comment:
|
|
242
|
+
cellinfo["Comment"] = cell.comment.text
|
|
243
|
+
cellinfo["Comment_Author"] = cell.comment.author
|
|
244
|
+
celltxt += f" Comment: {cell.comment.text}\n"
|
|
245
|
+
celltxt += f" Comment Author: {cell.comment.author}\n"
|
|
246
|
+
|
|
247
|
+
# フォント
|
|
248
|
+
font = cell.font
|
|
249
|
+
cellinfo["Font_Name"] = font.name
|
|
250
|
+
cellinfo["Font_Size"] = font.size
|
|
251
|
+
cellinfo["Font_Bold"] = font.bold
|
|
252
|
+
cellinfo["Font_Italic"] = font.italic
|
|
253
|
+
cellinfo["Font_Underline"] = font.underline
|
|
254
|
+
celltxt += f" Font Name: {font.name}\n"
|
|
255
|
+
celltxt += f" Font Size: {font.size}\n"
|
|
256
|
+
celltxt += f" Font Bold: {font.bold}\n"
|
|
257
|
+
celltxt += f" Font Italic: {font.italic}\n"
|
|
258
|
+
celltxt += f" Font Underline: {font.underline}\n"
|
|
259
|
+
if font.color:
|
|
260
|
+
if hasattr(font.color, "rgb") and font.color.rgb:
|
|
261
|
+
cellinfo["Font_Color_RGB"] = str(font.color.rgb)
|
|
262
|
+
celltxt += f" Font Color (RGB): {font.color.rgb}\n"
|
|
263
|
+
elif hasattr(font.color, "theme") and font.color.theme is not None:
|
|
264
|
+
cellinfo["Font_Color_Theme"] = str(font.color.theme)
|
|
265
|
+
celltxt += f" Font Color (Theme): {font.color.theme}\n"
|
|
266
|
+
else:
|
|
267
|
+
cellinfo["Font_Color"] = str(font.color)
|
|
268
|
+
celltxt += f" Font Color: {font.color}\n"
|
|
269
|
+
else:
|
|
270
|
+
cellinfo["Font_Color"] = "Default"
|
|
271
|
+
celltxt += f" Font Color: Default\n"
|
|
272
|
+
|
|
273
|
+
fill = cell.fill
|
|
274
|
+
if hasattr(fill, "patternType") and fill.patternType:
|
|
275
|
+
cellinfo["Fill_Pattern_Type"] = fill.patternType
|
|
276
|
+
celltxt += f" Fill Pattern Type: {fill.patternType}\n"
|
|
277
|
+
|
|
278
|
+
if hasattr(fill, "fgColor") and fill.fgColor:
|
|
279
|
+
if hasattr(fill.fgColor, "rgb") and fill.fgColor.rgb:
|
|
280
|
+
cellinfo["Fill_Foreground_Color_RGB"] = str(fill.fgColor.rgb)
|
|
281
|
+
celltxt += f" Fill Foreground Color (RGB): {fill.fgColor.rgb}\n"
|
|
282
|
+
elif hasattr(fill.fgColor, "theme") and fill.fgColor.theme is not None:
|
|
283
|
+
cellinfo["Fill_Foreground_Color_Theme"] = str(fill.fgColor.theme)
|
|
284
|
+
celltxt += f" Fill Foreground Color (Theme): {fill.fgColor.theme}\n"
|
|
285
|
+
else:
|
|
286
|
+
cellinfo["Fill_Foreground_Color"] = str(fill.fgColor)
|
|
287
|
+
celltxt += f" Fill Foreground Color: {fill.fgColor}\n"
|
|
288
|
+
|
|
289
|
+
if hasattr(fill, "bgColor") and fill.bgColor:
|
|
290
|
+
if hasattr(fill.bgColor, "rgb") and fill.bgColor.rgb:
|
|
291
|
+
cellinfo["Fill_Background_Color_RGB"] = str(fill.bgColor.rgb)
|
|
292
|
+
celltxt += f" Fill Background Color (RGB): {fill.bgColor.rgb}\n"
|
|
293
|
+
elif hasattr(fill.bgColor, "theme") and fill.bgColor.theme is not None:
|
|
294
|
+
cellinfo["Fill_Background_Color_Theme"] = str(fill.bgColor.theme)
|
|
295
|
+
celltxt += f" Fill Background Color (Theme): {fill.bgColor.theme}\n"
|
|
296
|
+
else:
|
|
297
|
+
cellinfo["Fill_Background_Color"] = str(fill.bgColor)
|
|
298
|
+
celltxt += f" Fill Background Color: {fill.bgColor}\n"
|
|
299
|
+
else:
|
|
300
|
+
cellinfo["Fill"] = "No fill pattern"
|
|
301
|
+
celltxt += f" Fill: No fill pattern\n"
|
|
302
|
+
|
|
303
|
+
# 配置
|
|
304
|
+
if hasattr(cell, "alignment") and cell.alignment:
|
|
305
|
+
alignment = cell.alignment
|
|
306
|
+
cellinfo["Horizontal_Alignment"] = alignment.horizontal
|
|
307
|
+
cellinfo["Vertical_Alignment"] = alignment.vertical
|
|
308
|
+
cellinfo["Text_Rotation"] = alignment.textRotation
|
|
309
|
+
cellinfo["Wrap_Text"] = alignment.wrapText
|
|
310
|
+
cellinfo["Indent"] = alignment.indent
|
|
311
|
+
cellinfo["Shrink_to_Fit"] = alignment.shrinkToFit
|
|
312
|
+
celltxt += f" Horizontal Alignment: {alignment.horizontal}\n"
|
|
313
|
+
celltxt += f" Vertical Alignment: {alignment.vertical}\n"
|
|
314
|
+
celltxt += f" Text Rotation: {alignment.textRotation}\n"
|
|
315
|
+
celltxt += f" Wrap Text: {alignment.wrapText}\n"
|
|
316
|
+
celltxt += f" Indent: {alignment.indent}\n"
|
|
317
|
+
celltxt += f" Shrink to Fit: {alignment.shrinkToFit}\n"
|
|
318
|
+
|
|
319
|
+
# 罫線
|
|
320
|
+
if hasattr(cell, "border") and cell.border:
|
|
321
|
+
if cell.border:
|
|
322
|
+
border_sides = {
|
|
323
|
+
"left": cell.border.left,
|
|
324
|
+
"right": cell.border.right,
|
|
325
|
+
"top": cell.border.top,
|
|
326
|
+
"bottom": cell.border.bottom,
|
|
327
|
+
"diagonal": cell.border.diagonal,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for side_name, side in border_sides.items():
|
|
331
|
+
b_name = f"Border {side_name.capitalize()}"
|
|
332
|
+
celltxt += f" {b_name}:\n"
|
|
333
|
+
if side and side.style:
|
|
334
|
+
cellinfo[f"{b_name}_Style"] = side.style
|
|
335
|
+
celltxt += f" Style: {side.style}\n"
|
|
336
|
+
if side.color:
|
|
337
|
+
if hasattr(side.color, "rgb") and side.color.rgb:
|
|
338
|
+
cellinfo[f"{b_name}_Color_RGB"] = str(side.color.rgb)
|
|
339
|
+
celltxt += f" Color (RGB): {side.color.rgb}\n"
|
|
340
|
+
elif hasattr(side.color, "theme") and side.color.theme is not None:
|
|
341
|
+
cellinfo[f"{b_name}_Color_Theme"] = str(side.color.theme)
|
|
342
|
+
celltxt += f" Color (Theme): {side.color.theme}\n"
|
|
343
|
+
else:
|
|
344
|
+
cellinfo[f"{b_name}_Color"] = str(side.color)
|
|
345
|
+
celltxt += f" Color: {side.color}\n"
|
|
346
|
+
|
|
347
|
+
# セルの保護
|
|
348
|
+
if hasattr(cell, "protection") and cell.protection:
|
|
349
|
+
if cell.protection:
|
|
350
|
+
cellinfo["Is_Cell_Locked"] = cell.protection.locked
|
|
351
|
+
cellinfo["Is_Cell_Hidden"] = cell.protection.hidden
|
|
352
|
+
celltxt += f" Is Cell Locked: {cell.protection.locked}\n"
|
|
353
|
+
celltxt += f" Is Cell Hidden: {cell.protection.hidden}\n"
|
|
354
|
+
|
|
355
|
+
# 条件付き書式
|
|
356
|
+
if hasattr(cell, "conditional_formatting") and cell.conditional_formatting:
|
|
357
|
+
cf_rules = []
|
|
358
|
+
for rule in sheet.conditional_formatting:
|
|
359
|
+
if f"P{cell.row}" in rule.cells.ranges:
|
|
360
|
+
cf_rules.append(rule)
|
|
361
|
+
|
|
362
|
+
if cf_rules:
|
|
363
|
+
cellinfo["Conditional_Formatting_Rules"] = []
|
|
364
|
+
for i, rule in enumerate(cf_rules):
|
|
365
|
+
rule_info = {"Rule_Index": i + 1, "SubRules": []}
|
|
366
|
+
cellinfo["Conditional_Formatting_Rules"].append(rule_info)
|
|
367
|
+
celltxt += f" Conditional Formatting Rule {i + 1}:\n"
|
|
368
|
+
for subrule in rule.rules:
|
|
369
|
+
subrule_info = {"Type": type(subrule).__name__}
|
|
370
|
+
rule_info["SubRules"].append(subrule_info)
|
|
371
|
+
celltxt += f" SubRule Type: {type(subrule).__name__}\n"
|
|
372
|
+
if hasattr(subrule, "formula"):
|
|
373
|
+
subrule_info["Formula"] = subrule.formula
|
|
374
|
+
celltxt += f" Formula: {subrule.formula}\n"
|
|
375
|
+
if hasattr(subrule, "operator"):
|
|
376
|
+
subrule_info["Operator"] = subrule.operator
|
|
377
|
+
celltxt += f" Operator: {subrule.operator}\n"
|
|
378
|
+
if hasattr(subrule, "dxf") and subrule.dxf:
|
|
379
|
+
subrule_info["Differential_Style"] = {}
|
|
380
|
+
celltxt += f" Differential Style:\n"
|
|
381
|
+
if hasattr(subrule.dxf, "font") and subrule.dxf.font:
|
|
382
|
+
subrule_info["Differential_Style"]["Font"] = subrule.dxf.font
|
|
383
|
+
celltxt += f" Font: {subrule.dxf.font}\n"
|
|
384
|
+
if hasattr(subrule.dxf, "fill") and subrule.dxf.fill:
|
|
385
|
+
subrule_info["Differential_Style"]["Fill"] = subrule.dxf.fill
|
|
386
|
+
celltxt += f" Fill: {subrule.dxf.fill}\n"
|
|
387
|
+
if hasattr(subrule.dxf, "border") and subrule.dxf.border:
|
|
388
|
+
subrule_info["Differential_Style"]["Border"] = subrule.dxf.border
|
|
389
|
+
celltxt += f" Border: {subrule.dxf.border}\n"
|
|
390
|
+
|
|
391
|
+
# merged cells
|
|
392
|
+
if hasattr(sheet, "merged_cells") and sheet.merged_cells:
|
|
393
|
+
celltxt += f" Merged Cells:\n"
|
|
394
|
+
for merged_range in sheet.merged_cells.ranges:
|
|
395
|
+
if cell.coordinate in merged_range:
|
|
396
|
+
cellinfo["Cell_is_part_of_merged_range"] = {
|
|
397
|
+
"Merge_starts_at": {"min_row": merged_range.min_row, "min_col": merged_range.min_col},
|
|
398
|
+
"Merge_ends_at": {"max_row": merged_range.max_row, "max_col": merged_range.max_col}
|
|
399
|
+
}
|
|
400
|
+
celltxt += f" Merge starts at: (Row: {merged_range.min_row}, Column: {merged_range.min_col})\n"
|
|
401
|
+
celltxt += f" Merge ends at: (Row: {merged_range.max_row}, Column: {merged_range.max_col})\n"
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
return celltxt
|
|
405
|
+
|
|
406
|
+
cellinfos = {}
|
|
407
|
+
celltxt = ""
|
|
408
|
+
sheet:Worksheet = wb[sheet_name]
|
|
409
|
+
if cell_name is not None and len(cell_name) > 0:
|
|
410
|
+
for cn in cell_name:
|
|
411
|
+
celltxt += _proc(cellinfos, sheet, cn)
|
|
412
|
+
|
|
413
|
+
if cell_top_left is not None and cell_bottom_right is not None:
|
|
414
|
+
range_str = ":".join(sorted([cell_top_left, cell_bottom_right]))
|
|
415
|
+
cell_range = sheet[range_str]
|
|
416
|
+
for row in cell_range:
|
|
417
|
+
for cell in row:
|
|
418
|
+
if hasattr(cell, 'coordinate'):
|
|
419
|
+
celltxt += _proc(cellinfos, sheet, cell.coordinate)
|
|
420
|
+
|
|
421
|
+
if (cell_name is None or len(cell_name) <= 0) \
|
|
422
|
+
and cell_top_left is None and cell_bottom_right is None:
|
|
423
|
+
for row in sheet.iter_rows():
|
|
424
|
+
for cell in row:
|
|
425
|
+
if hasattr(cell, 'coordinate'):
|
|
426
|
+
celltxt += _proc(cellinfos, sheet, cell.coordinate)
|
|
427
|
+
|
|
428
|
+
res = dict(success={sheet_name:cellinfos if output_detail_format=='json' else celltxt})
|
|
429
|
+
return res
|
|
430
|
+
except Exception as e:
|
|
431
|
+
msg = dict(warn=f"Failed to cell details: {e}")
|
|
432
|
+
logger.warning(f"Failed to cell details: {e}", exc_info=True)
|
|
433
|
+
return msg
|
|
434
|
+
finally:
|
|
435
|
+
if wb is not None:
|
|
436
|
+
wb.close()
|