cmdbox 0.5.1.2__py3-none-any.whl → 0.5.3__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.

Files changed (143) hide show
  1. cmdbox/app/app.py +4 -2
  2. cmdbox/app/auth/signin.py +634 -631
  3. cmdbox/app/client.py +10 -10
  4. cmdbox/app/common.py +50 -6
  5. cmdbox/app/commons/convert.py +9 -0
  6. cmdbox/app/commons/module.py +113 -113
  7. cmdbox/app/commons/redis_client.py +40 -29
  8. cmdbox/app/edge.py +4 -4
  9. cmdbox/app/features/cli/audit_base.py +138 -0
  10. cmdbox/app/features/cli/cmdbox_audit_createdb.py +224 -0
  11. cmdbox/app/features/cli/cmdbox_audit_delete.py +308 -0
  12. cmdbox/app/features/cli/cmdbox_audit_search.py +416 -0
  13. cmdbox/app/features/cli/cmdbox_audit_write.py +247 -0
  14. cmdbox/app/features/cli/cmdbox_client_file_copy.py +207 -207
  15. cmdbox/app/features/cli/cmdbox_client_file_download.py +207 -207
  16. cmdbox/app/features/cli/cmdbox_client_file_list.py +193 -193
  17. cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +191 -191
  18. cmdbox/app/features/cli/cmdbox_client_file_move.py +199 -199
  19. cmdbox/app/features/cli/cmdbox_client_file_remove.py +190 -190
  20. cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +190 -190
  21. cmdbox/app/features/cli/cmdbox_client_file_upload.py +212 -212
  22. cmdbox/app/features/cli/cmdbox_client_server_info.py +166 -166
  23. cmdbox/app/features/cli/cmdbox_server_list.py +88 -88
  24. cmdbox/app/features/cli/cmdbox_server_stop.py +138 -138
  25. cmdbox/app/features/web/cmdbox_web_audit.py +81 -0
  26. cmdbox/app/features/web/cmdbox_web_audit_metrics.py +72 -0
  27. cmdbox/app/features/web/cmdbox_web_del_cmd.py +2 -0
  28. cmdbox/app/features/web/cmdbox_web_del_pipe.py +1 -0
  29. cmdbox/app/features/web/cmdbox_web_do_signin.py +12 -2
  30. cmdbox/app/features/web/cmdbox_web_do_signout.py +1 -0
  31. cmdbox/app/features/web/cmdbox_web_exec_cmd.py +31 -2
  32. cmdbox/app/features/web/cmdbox_web_exec_pipe.py +1 -0
  33. cmdbox/app/features/web/cmdbox_web_filer download.py +43 -42
  34. cmdbox/app/features/web/cmdbox_web_filer.py +1 -0
  35. cmdbox/app/features/web/cmdbox_web_filer_upload.py +65 -64
  36. cmdbox/app/features/web/cmdbox_web_gui.py +166 -165
  37. cmdbox/app/features/web/cmdbox_web_load_pin.py +43 -43
  38. cmdbox/app/features/web/cmdbox_web_raw_pipe.py +87 -87
  39. cmdbox/app/features/web/cmdbox_web_save_cmd.py +1 -0
  40. cmdbox/app/features/web/cmdbox_web_save_pin.py +42 -42
  41. cmdbox/app/features/web/cmdbox_web_save_pipe.py +1 -0
  42. cmdbox/app/features/web/cmdbox_web_user_data.py +58 -0
  43. cmdbox/app/features/web/cmdbox_web_users.py +12 -0
  44. cmdbox/app/options.py +788 -601
  45. cmdbox/app/web.py +7 -1
  46. cmdbox/extensions/features.yml +23 -0
  47. cmdbox/extensions/sample_project/sample/app/features/cli/sample_client_time.py +82 -82
  48. cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +145 -145
  49. cmdbox/extensions/user_list.yml +5 -0
  50. cmdbox/licenses/{LICENSE.Sphinx.8.1.3(BSD License).txt → LICENSE.Sphinx.8.2.3(UNKNOWN).txt} +1 -1
  51. cmdbox/licenses/LICENSE.argcomplete.3.6.2(Apache Software License).txt +177 -0
  52. cmdbox/licenses/{LICENSE.babel.2.16.0(BSD License).txt → LICENSE.babel.2.17.0(BSD License).txt } +1 -1
  53. cmdbox/licenses/{LICENSE.pkginfo.1.10.0(MIT License).txt → LICENSE.charset-normalizer.3.4.1(MIT License).txt } +1 -1
  54. cmdbox/licenses/LICENSE.gevent.25.4.1(MIT).txt +25 -0
  55. cmdbox/licenses/LICENSE.greenlet.3.2.0(MIT AND Python-2.0).txt +30 -0
  56. cmdbox/licenses/LICENSE.gunicorn.23.0.0(MIT License).txt +23 -0
  57. cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +202 -0
  58. cmdbox/licenses/LICENSE.nh3.0.2.21(MIT).txt +21 -0
  59. cmdbox/licenses/{LICENSE.pillow.11.0.0(CMU License (MIT-CMU)).txt → LICENSE.pillow.11.1.0(CMU License (MIT-CMU)).txt } +27 -40
  60. cmdbox/licenses/LICENSE.pillow.11.2.1(UNKNOWN).txt +1200 -0
  61. cmdbox/licenses/LICENSE.plyer.2.1.0(MIT License).txt +19 -0
  62. cmdbox/licenses/LICENSE.prompt_toolkit.3.0.50(BSD License).txt +27 -0
  63. cmdbox/licenses/LICENSE.prompt_toolkit.3.0.51(BSD License).txt +27 -0
  64. cmdbox/licenses/LICENSE.psycopg-binary.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
  65. cmdbox/licenses/LICENSE.psycopg-pool.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
  66. cmdbox/licenses/LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt +165 -0
  67. cmdbox/licenses/LICENSE.pycryptodome.3.22.0(BSD License; Public Domain).txt +61 -0
  68. cmdbox/licenses/LICENSE.pydantic.2.11.3(MIT License).txt +21 -0
  69. cmdbox/licenses/LICENSE.pydantic_core.2.33.1(MIT License).txt +21 -0
  70. cmdbox/licenses/LICENSE.pystray.0.19.5(GNU Lesser General Public License v3 (LGPLv3)).txt +674 -0
  71. cmdbox/licenses/LICENSE.questionary.2.1.0(MIT License).txt +19 -0
  72. cmdbox/licenses/LICENSE.roman-numerals-py.3.1.0(CC0 1.0 Universal (CC0 1.0) Public Domain Dedication; Zero-Clause BSD (0BSD)).txt +146 -0
  73. cmdbox/licenses/{LICENSE.six.1.16.0(MIT License).txt → LICENSE.six.1.17.0(MIT License).txt } +1 -1
  74. cmdbox/licenses/LICENSE.starlette.0.46.2(BSD License).txt +27 -0
  75. cmdbox/licenses/{LICENSE.charset-normalizer.3.4.0(MIT License).txt → LICENSE.typing-inspection.0.4.0(MIT License).txt } +2 -2
  76. cmdbox/licenses/LICENSE.typing_extensions.4.13.2(UNKNOWN).txt +279 -0
  77. cmdbox/licenses/LICENSE.tzdata.2025.2(Apache Software License).txt +15 -0
  78. cmdbox/licenses/LICENSE.urllib3.2.4.0(UNKNOWN).txt +21 -0
  79. cmdbox/licenses/LICENSE.uvicorn.0.34.1(BSD License).txt +27 -0
  80. cmdbox/licenses/LICENSE.watchfiles.1.0.5(MIT License).txt +21 -0
  81. cmdbox/licenses/files.txt +49 -38
  82. cmdbox/logconf_audit.yml +30 -0
  83. cmdbox/logconf_cmdbox.yml +30 -0
  84. cmdbox/version.py +2 -2
  85. cmdbox/web/assets/apexcharts/apexcharts.css +679 -0
  86. cmdbox/web/assets/apexcharts/apexcharts.min.js +38 -0
  87. cmdbox/web/assets/cmdbox/audit.js +340 -0
  88. cmdbox/web/assets/cmdbox/color_mode.css +520 -0
  89. cmdbox/web/assets/cmdbox/common.js +416 -24
  90. cmdbox/web/assets/cmdbox/filer_modal.js +1 -1
  91. cmdbox/web/assets/cmdbox/list_cmd.js +10 -275
  92. cmdbox/web/assets/cmdbox/list_pipe.js +3 -3
  93. cmdbox/web/assets/cmdbox/main.js +2 -2
  94. cmdbox/web/assets/cmdbox/result.js +2 -2
  95. cmdbox/web/assets/cmdbox/signin.js +2 -2
  96. cmdbox/web/assets/cmdbox/users.js +19 -20
  97. cmdbox/web/assets/cmdbox/view_raw.js +1 -1
  98. cmdbox/web/assets/cmdbox/view_result.js +11 -13
  99. cmdbox/web/assets/filer/filer.js +2 -2
  100. cmdbox/web/assets/filer/main.js +2 -2
  101. cmdbox/web/assets_license_list.txt +4 -1
  102. cmdbox/web/audit.html +268 -0
  103. cmdbox/web/filer.html +37 -12
  104. cmdbox/web/gui.html +36 -53
  105. cmdbox/web/result.html +24 -3
  106. cmdbox/web/signin.html +35 -14
  107. cmdbox/web/users.html +21 -3
  108. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/METADATA +28 -5
  109. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/RECORD +142 -103
  110. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/entry_points.txt +0 -1
  111. cmdbox/licenses/LICENSE.nh3.0.2.18(MIT).txt +0 -1
  112. /cmdbox/licenses/{LICENSE.Jinja2.3.1.4(BSD License).txt → LICENSE.Jinja2.3.1.6(BSD License).txt} +0 -0
  113. /cmdbox/licenses/{LICENSE.Pygments.2.18.0(BSD License).txt → LICENSE.Pygments.2.19.1(BSD License).txt} +0 -0
  114. /cmdbox/licenses/{LICENSE.anyio.4.6.2.post1(MIT License).txt → LICENSE.anyio.4.9.0(MIT License).txt} +0 -0
  115. /cmdbox/licenses/{LICENSE.argcomplete.3.5.1(Apache Software License).txt → LICENSE.argcomplete.3.6.1(Apache Software License).txt} +0 -0
  116. /cmdbox/licenses/{LICENSE.certifi.2024.8.30(Mozilla Public License 2.0 (MPL 2.0)).txt → LICENSE.certifi.2025.1.31(Mozilla Public License 2.0 (MPL 2.0)).txt} +0 -0
  117. /cmdbox/licenses/{LICENSE.click.8.1.7(BSD License).txt → LICENSE.click.8.1.8(BSD License).txt} +0 -0
  118. /cmdbox/licenses/{LICENSE.cryptography.43.0.3(Apache Software License; BSD License).txt → LICENSE.cryptography.44.0.2(Apache Software License; BSD License).txt} +0 -0
  119. /cmdbox/licenses/{LICENSE.fastapi.0.115.5(MIT License).txt → LICENSE.fastapi.0.115.12(MIT License).txt} +0 -0
  120. /cmdbox/licenses/{LICENSE.importlib_metadata.8.5.0(Apache Software License).txt → LICENSE.id.1.5.0(Apache Software License).txt} +0 -0
  121. /cmdbox/licenses/{LICENSE.keyring.25.5.0(MIT License).txt → LICENSE.keyring.25.6.0(MIT License).txt} +0 -0
  122. /cmdbox/licenses/{LICENSE.more-itertools.10.5.0(MIT License).txt → LICENSE.more-itertools.10.6.0(MIT License).txt} +0 -0
  123. /cmdbox/licenses/{LICENSE.numpy.2.1.3(BSD License).txt → LICENSE.numpy.2.2.4(BSD License).txt} +0 -0
  124. /cmdbox/licenses/{LICENSE.prettytable.3.12.0(BSD License).txt → LICENSE.prettytable.3.16.0(UNKNOWN).txt} +0 -0
  125. /cmdbox/licenses/{LICENSE.pydantic.2.10.2(MIT License).txt → LICENSE.pydantic.2.11.1(MIT License).txt} +0 -0
  126. /cmdbox/licenses/{LICENSE.pydantic_core.2.27.1(MIT License).txt → LICENSE.pydantic_core.2.33.0(MIT License).txt} +0 -0
  127. /cmdbox/licenses/{LICENSE.python-dotenv.1.0.1(BSD License).txt → LICENSE.python-dotenv.1.1.0(BSD License).txt} +0 -0
  128. /cmdbox/licenses/{LICENSE.python-multipart.0.0.17(Apache Software License).txt → LICENSE.python-multipart.0.0.20(Apache Software License).txt} +0 -0
  129. /cmdbox/licenses/{LICENSE.redis.5.2.0(MIT License).txt → LICENSE.redis.5.2.1(MIT License).txt} +0 -0
  130. /cmdbox/licenses/{LICENSE.rich.13.9.4(MIT License).txt → LICENSE.rich.14.0.0(MIT License).txt} +0 -0
  131. /cmdbox/licenses/{LICENSE.sphinx-intl.2.3.0(BSD License).txt → LICENSE.sphinx-intl.2.3.1(BSD License).txt} +0 -0
  132. /cmdbox/licenses/{LICENSE.starlette.0.41.3(BSD License).txt → LICENSE.starlette.0.46.1(BSD License).txt} +0 -0
  133. /cmdbox/licenses/{LICENSE.tomli.2.1.0(MIT License).txt → LICENSE.tomli.2.2.1(MIT License).txt} +0 -0
  134. /cmdbox/licenses/{LICENSE.twine.5.1.1(Apache Software License).txt → LICENSE.twine.6.1.0(Apache Software License).txt} +0 -0
  135. /cmdbox/licenses/{LICENSE.typing_extensions.4.12.2(Python Software Foundation License).txt → LICENSE.typing_extensions.4.13.0(UNKNOWN).txt} +0 -0
  136. /cmdbox/licenses/{LICENSE.urllib3.2.2.3(MIT License).txt → LICENSE.urllib3.2.3.0(MIT License).txt} +0 -0
  137. /cmdbox/licenses/{LICENSE.uvicorn.0.32.1(BSD License).txt → LICENSE.uvicorn.0.34.0(BSD License).txt} +0 -0
  138. /cmdbox/licenses/{LICENSE.watchfiles.1.0.0(MIT License).txt → LICENSE.watchfiles.1.0.4(MIT License).txt} +0 -0
  139. /cmdbox/licenses/{LICENSE.websockets.14.1(BSD License).txt → LICENSE.websockets.15.0.1(BSD License).txt} +0 -0
  140. /cmdbox/licenses/{LICENSE.zope.interface.7.1.1(Zope Public License).txt → LICENSE.zope.interface.7.2(Zope Public License).txt} +0 -0
  141. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/LICENSE +0 -0
  142. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/WHEEL +0 -0
  143. {cmdbox-0.5.1.2.dist-info → cmdbox-0.5.3.dist-info}/top_level.txt +0 -0
cmdbox/app/options.py CHANGED
@@ -1,601 +1,788 @@
1
- from cmdbox.app import common, feature
2
- from cmdbox.app.commons import module
3
- from fastapi.routing import APIRoute
4
- from pathlib import Path
5
- from starlette.routing import Route
6
- from typing import List, Dict, Any
7
- import locale
8
- import logging
9
- import re
10
-
11
-
12
- class Options:
13
- T_INT = 'int'
14
- T_FLOAT = 'float'
15
- T_BOOL = 'bool'
16
- T_STR = 'str'
17
- T_DICT = 'dict'
18
- T_TEXT = 'text'
19
- T_FILE = 'file'
20
- T_DIR = 'dir'
21
-
22
- def __setattr__(self, name:str, value):
23
- if name.startswith("T_") and name in self.__dict__:
24
- raise ValueError(f'Cannot set attribute. ({name})')
25
- self.__dict__[name] = value
26
-
27
- _instance = None
28
-
29
- @staticmethod
30
- def getInstance(appcls=None, ver=None):
31
- if Options._instance is None:
32
- Options._instance = Options(appcls=appcls, ver=ver)
33
- return Options._instance
34
-
35
- def __init__(self, appcls=None, ver=None):
36
- self.appcls = appcls
37
- self.ver = ver
38
- self.features_yml_data = None
39
- self.features_loaded = dict()
40
- self.aliases_loaded_cli = False
41
- self.aliases_loaded_web = False
42
- self.init_options()
43
-
44
- def get_mode_keys(self) -> List[str]:
45
- return [key for key,val in self._options["mode"].items() if type(val) == dict]
46
-
47
- def get_modes(self) -> List[Dict[str, str]]:
48
- """
49
- 起動モードの選択肢を取得します。
50
- Returns:
51
- List[Dict[str, str]]: 起動モードの選択肢
52
- """
53
- return [''] + [{key:val} for key,val in self._options["mode"].items() if type(val) == dict]
54
-
55
- def get_cmd_keys(self, mode:str) -> List[str]:
56
- if mode not in self._options["cmd"]:
57
- return []
58
- return [key for key,val in self._options["cmd"][mode].items() if type(val) == dict]
59
-
60
- def get_cmds(self, mode:str) -> List[Dict[str, str]]:
61
- """
62
- コマンドの選択肢を取得します。
63
- Args:
64
- mode: 起動モード
65
- Returns:
66
- List[Dict[str, str]]: コマンドの選択肢
67
- """
68
- if mode not in self._options["cmd"]:
69
- return ['Please select mode.']
70
- ret = [{key:val} for key,val in self._options["cmd"][mode].items() if type(val) == dict]
71
- if len(ret) > 0:
72
- return [''] + ret
73
- return ['Please select mode.']
74
-
75
- def get_cmd_attr(self, mode:str, cmd:str, attr:str) -> Any:
76
- """
77
- コマンドの属性を取得します。
78
- Args:
79
- mode: 起動モード
80
- cmd: コマンド
81
- attr: 属性
82
- Returns:
83
- Any: 属性の値
84
- """
85
- if mode not in self._options["cmd"]:
86
- return [f'Unknown mode. ({mode})']
87
- if cmd is None or cmd == "" or cmd not in self._options["cmd"][mode]:
88
- return []
89
- if attr not in self._options["cmd"][mode][cmd]:
90
- return None
91
- return self._options["cmd"][mode][cmd][attr]
92
-
93
- def get_svcmd_feature(self, svcmd:str) -> Any:
94
- """
95
- サーバー側のコマンドのフューチャーを取得します。
96
-
97
- Args:
98
- svcmd: サーバー側のコマンド
99
- Returns:
100
- feature.Feature: フューチャー
101
- """
102
- if svcmd is None or svcmd == "":
103
- return None
104
- if svcmd not in self._options["svcmd"]:
105
- return None
106
- return self._options["svcmd"][svcmd]
107
-
108
- def get_cmd_choices(self, mode:str, cmd:str, webmode:bool=False) -> List[Dict[str, Any]]:
109
- """
110
- コマンドのオプション一覧を取得します。
111
- Args:
112
- mode: 起動モード
113
- cmd: コマンド
114
- webmode (bool, optional): Webモードからの呼び出し. Defaults to False
115
- Returns:
116
- List[Dict[str, Any]]: オプションの選択肢
117
- """
118
- opts = self.get_cmd_attr(mode, cmd, "choice")
119
- ret = []
120
- for o in opts:
121
- if not webmode or type(o) is not dict:
122
- ret.append(o)
123
- continue
124
- o = o.copy()
125
- if 'web' in o and o['web'] == 'mask':
126
- o['default'] = '********'
127
- ret.append(o)
128
- return ret
129
-
130
- def get_cmd_opt(self, mode:str, cmd:str, opt:str, webmode:bool=False) -> Dict[str, Any]:
131
- """
132
- コマンドのオプションを取得します。
133
- Args:
134
- mode: 起動モード
135
- cmd: コマンド
136
- opt: オプション
137
- webmode (bool, optional): Webモードからの呼び出し. Defaults to False
138
- Returns:
139
- Dict[str, Any]: オプションの値
140
- """
141
- opts = self.get_cmd_choices(mode, cmd, webmode)
142
- for o in opts:
143
- if 'opt' in o and o['opt'] == opt:
144
- return o
145
- return None
146
-
147
- def list_options(self):
148
- def _list(ret, key, val):
149
- if type(val) != dict or 'type' not in val:
150
- return
151
- opt = dict()
152
- if val['type'] == Options.T_INT:
153
- opt['type'] = int
154
- opt['action'] = 'append' if val['multi'] else None
155
- elif val['type'] == Options.T_FLOAT:
156
- opt['type'] = float
157
- opt['action'] = 'append' if val['multi'] else None
158
- elif val['type'] == Options.T_BOOL:
159
- opt['type'] = bool
160
- opt['action'] = 'store_true'
161
- elif val['type'] == Options.T_DICT:
162
- opt['type'] = dict
163
- if not val['multi']:
164
- raise ValueError(f'list_options: The multi must be True if type is dict. key={key}, val={val}')
165
- opt['action'] = 'append'
166
- else:
167
- opt['type'] = str
168
- opt['action'] = 'append' if val['multi'] else None
169
- o = [f'-{val["short"]}'] if "short" in val else []
170
- o += [f'--{key}']
171
- language, _ = locale.getlocale()
172
- opt['help'] = val['discription_en'] if language.find('Japan') < 0 and language.find('ja_JP') < 0 else val['discription_ja']
173
- opt['default'] = val['default']
174
- if val['multi'] and val['default'] is not None:
175
- raise ValueError(f'list_options: The default value must be None if multi is True. key={key}, val={val}')
176
- opt['opts'] = o
177
- if val['choice'] is not None:
178
- opt['choices'] = []
179
- for c in val['choice']:
180
- if type(c) == dict:
181
- opt['choices'] += [c['opt']]
182
- elif c is not None and c != "":
183
- opt['choices'] += [c]
184
- else:
185
- opt['choices'] = None
186
- ret[key] = opt
187
- ret = dict()
188
- for k, v in self._options.items():
189
- _list(ret, k, v)
190
- #for mode in self._options["mode"]['choice']:
191
- for _, cmd in self._options["cmd"].items():
192
- if type(cmd) is not dict:
193
- continue
194
- for _, opt in cmd.items():
195
- if type(opt) is not dict:
196
- continue
197
- for o in opt["choice"]:
198
- if type(o) is not dict:
199
- continue
200
- _list(ret, o['opt'], o)
201
- return ret
202
-
203
- def mk_opt_list(self, opt:dict, webmode:bool=False) -> List[str]:
204
- opt_schema = self.get_cmd_choices(opt['mode'], opt['cmd'], webmode)
205
- opt_list = ['-m', opt['mode'], '-c', opt['cmd']]
206
- file_dict = dict()
207
- for key, val in opt.items():
208
- if key in ['stdout_log', 'capture_stdout']:
209
- continue
210
- schema = [schema for schema in opt_schema if type(schema) is dict and schema['opt'] == key]
211
- if len(schema) == 0 or val == '':
212
- continue
213
- if schema[0]['type'] == Options.T_BOOL:
214
- if val:
215
- opt_list.append(f"--{key}")
216
- continue
217
- if type(val) == list:
218
- for v in val:
219
- if v is None or v == '':
220
- continue
221
- opt_list.append(f"--{key}")
222
- if str(v).find(' ') >= 0:
223
- opt_list.append(f'"{v}"')
224
- else:
225
- opt_list.append(str(v))
226
- elif type(val) == dict:
227
- for k,v in val.items():
228
- if k is None or k == '' or v is None or v == '':
229
- continue
230
- opt_list.append(f"--{key}")
231
- k = f'"{k}"' if str(k).find(' ') >= 0 else str(k)
232
- v = f'"{v}"' if str(v).find(' ') >= 0 else str(v)
233
- opt_list.append(f'{k}={v}')
234
- elif val is not None and val != '':
235
- opt_list.append(f"--{key}")
236
- if str(val).find(' ') >= 0:
237
- opt_list.append(f'"{val}"')
238
- else:
239
- opt_list.append(str(val))
240
- if 'fileio' in schema[0] and schema[0]['fileio'] == 'in' and type(val) != str:
241
- file_dict[key] = val
242
- return opt_list, file_dict
243
-
244
- def init_options(self):
245
- self._options = dict()
246
- self._options["version"] = dict(
247
- short="v", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=None,
248
- discription_ja="バージョン表示",
249
- discription_en="Display version")
250
- self._options["useopt"] = dict(
251
- short="u", type=Options.T_STR, default=None, required=False, multi=False, hide=True, choice=None,
252
- discription_ja="オプションを保存しているファイルを使用します。",
253
- discription_en="Use the file that saves the options.")
254
- self._options["saveopt"] = dict(
255
- short="s", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=[True, False],
256
- discription_ja="指定しているオプションを `-u` で指定したファイルに保存します。",
257
- discription_en="Save the specified options to the file specified by `-u`.")
258
- self._options["debug"] = dict(
259
- short="d", type=Options.T_BOOL, default=False, required=False, multi=False, hide=True, choice=[True, False],
260
- discription_ja="デバックモードで起動します。",
261
- discription_en="Starts in debug mode.")
262
- self._options["format"] = dict(
263
- short="f", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True,
264
- discription_ja="処理結果を見やすい形式で出力します。指定しない場合json形式で出力します。",
265
- discription_en="Output the processing result in an easy-to-read format. If not specified, output in json format.",
266
- choice=None)
267
- self._options["mode"] = dict(
268
- short="m", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
269
- discription_ja="起動モードを指定します。",
270
- discription_en="Specify the startup mode.",
271
- choice=[])
272
- self._options["cmd"] = dict(
273
- short="c", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
274
- discription_ja="コマンドを指定します。",
275
- discription_en="Specify the command.",
276
- choice=[])
277
- self._options["tag"] = dict(
278
- short="t", type=Options.T_STR, default=None, required=False, multi=True, hide=True,
279
- discription_ja="このコマンドのタグを指定します。",
280
- discription_en="Specify the tag for this command.",
281
- choice=None)
282
-
283
- def init_debugoption(self):
284
- # デバックオプションを追加
285
- self._options["debug"]["opt"] = "debug"
286
- self._options["tag"]["opt"] = "tag"
287
- for key, mode in self._options["cmd"].items():
288
- if type(mode) is not dict:
289
- continue
290
- mode['opt'] = key
291
- for k, c in mode.items():
292
- if type(c) is not dict:
293
- continue
294
- c["opt"] = k
295
- if "debug" not in [_o['opt'] for _o in c["choice"]]:
296
- c["choice"].append(self._options["debug"])
297
- if "tag" not in [_o['opt'] for _o in c["choice"]]:
298
- c["choice"].append(self._options["tag"])
299
- if c["opt"] not in [_o['opt'] for _o in self._options["cmd"]["choice"]]:
300
- self._options["cmd"]["choice"] += [c]
301
- self._options["mode"][key] = mode
302
- self._options["mode"]["choice"] += [mode]
303
-
304
- def load_svcmd(self, package_name:str, prefix:str="cmdbox_", excludes:list=[], appcls=None, ver=None, logger:logging.Logger=None, isloaded:bool=True):
305
- """
306
- 指定されたパッケージの指定された接頭語を持つモジュールを読み込みます。
307
-
308
- Args:
309
- package_name (str): パッケージ名
310
- prefix (str): 接頭語
311
- excludes (list): 除外するモジュール名のリスト
312
- appcls (Any): アプリケーションクラス
313
- ver (Any): バージョンモジュール
314
- logger (logging.Logger): ロガー
315
- isloaded (bool): 読み込み済みかどうか
316
- """
317
- if "svcmd" not in self._options:
318
- self._options["svcmd"] = dict()
319
- for mode, f in module.load_features(package_name, prefix, excludes, appcls=appcls, ver=ver).items():
320
- if mode not in self._options["cmd"]:
321
- self._options["cmd"][mode] = dict()
322
- for cmd, opt in f.items():
323
- self._options["cmd"][mode][cmd] = opt
324
- fobj:feature.Feature = opt['feature']
325
- if not isloaded and logger is not None and logger.level == logging.DEBUG:
326
- logger.debug(f"loaded features: mode={mode}, cmd={cmd}, {fobj}")
327
- svcmd = fobj.get_svcmd()
328
- if svcmd is not None:
329
- self._options["svcmd"][svcmd] = fobj
330
- self.init_debugoption()
331
-
332
- def is_features_loaded(self, ftype:str) -> bool:
333
- """
334
- 指定されたフィーチャータイプが読み込まれているかどうかを返します。
335
-
336
- Args:
337
- ftype (str): フィーチャータイプ
338
- Returns:
339
- bool: 読み込まれているかどうか
340
- """
341
- return ftype in self.features_loaded and self.features_loaded[ftype]
342
-
343
- def load_features_file(self, ftype:str, func, appcls, ver, logger:logging.Logger=None):
344
- """
345
- フィーチャーファイル(features.yml)を読み込みます。
346
-
347
- Args:
348
- ftype (str): フィーチャータイプ。cli又はweb
349
- func (Any): フィーチャーの処理関数
350
- appcls (Any): アプリケーションクラス
351
- ver (Any): バージョンモジュール
352
- logger (logging.Logger): ロガー
353
- """
354
- # 読込み済みかどうかの判定
355
- if self.is_features_loaded(ftype):
356
- return
357
- # cmdboxを拡張したアプリをカスタマイズするときのfeatures.ymlを読み込む
358
- features_yml = Path('features.yml')
359
- if not features_yml.exists() or not features_yml.is_file():
360
- # cmdboxを拡張したアプリの組み込みfeatures.ymlを読み込む
361
- features_yml = Path(ver.__file__).parent / 'extensions' / 'features.yml'
362
- #if not features_yml.exists() or not features_yml.is_file():
363
- # features_yml = Path('.samples/features.yml')
364
- if logger is not None and logger.level == logging.DEBUG:
365
- logger.debug(f"load features.yml: {features_yml}, is_file={features_yml.is_file()}")
366
- if features_yml.exists() and features_yml.is_file():
367
- if self.features_yml_data is None:
368
- self.features_yml_data = yml = common.load_yml(features_yml)
369
- else:
370
- yml = self.features_yml_data
371
- if yml is None: return
372
- if 'features' not in yml:
373
- raise Exception('features.yml is invalid. (The root element must be "features".)')
374
- if ftype not in yml['features']:
375
- raise Exception(f'features.yml is invalid. (There is no “{ftype}” in the “features” element.)')
376
- if yml['features'][ftype] is None:
377
- return
378
- if type(yml['features'][ftype]) is not list:
379
- raise Exception(f'features.yml is invalid. (The features.{ftype} element must be a list. {ftype}={yml["features"][ftype]})')
380
- for data in yml['features'][ftype]:
381
- if type(data) is not dict:
382
- raise Exception(f'features.yml is invalid. (The “features.{ftype} element must be a list element must be a dictionary. data={data})')
383
- if 'package' not in data:
384
- raise Exception(f'features.yml is invalid. (The “package” element must be in the dictionary of the list element of the “features.{ftype}” element. data={data})')
385
- if 'prefix' not in data:
386
- raise Exception(f'features.yml is invalid. (The prefix element must be in the dictionary of the list element of the “features.{ftype}” element. data={data})')
387
- if data['package'] is None or data['package'] == "":
388
- continue
389
- if data['prefix'] is None or data['prefix'] == "":
390
- continue
391
- exclude_modules = []
392
- if 'exclude_modules' in data:
393
- if type(data['exclude_modules']) is not list:
394
- raise Exception(f'features.yml is invalid. (Theexclude_moduleselement must be a list element. data={data})')
395
- exclude_modules = data['exclude_modules']
396
- func(data['package'], data['prefix'], exclude_modules, appcls, ver, logger, self.is_features_loaded(ftype))
397
- self.features_loaded[ftype] = True
398
-
399
- def load_features_args(self, args_dict:Dict[str, Any]):
400
- yml = self.features_yml_data
401
- if yml is None:
402
- return
403
- if 'args' not in yml or 'cli' not in yml['args']:
404
- return
405
-
406
- opts = self.list_options()
407
- def _cast(self, key, val):
408
- for opt in opts.values():
409
- if f"--{key}" in opt['opts']:
410
- if opt['type'] == int:
411
- return int(val)
412
- elif opt['type'] == float:
413
- return float(val)
414
- elif opt['type'] == bool:
415
- return True
416
- else:
417
- return eval(val)
418
- return None
419
-
420
- for rule in yml['args']['cli']:
421
- if type(rule) is not dict:
422
- raise Exception(f'features.yml is invalid. (The “args.cli” element must be a list element must be a dictionary. rule={rule})')
423
- if 'rule' not in rule:
424
- raise Exception(f'features.yml is invalid. (The “rule” element must be in the dictionary of the list element of the “args.cli” element. rule={rule})')
425
- if rule['rule'] is None:
426
- continue
427
- if 'default' not in rule and 'coercion' not in rule:
428
- raise Exception(f'features.yml is invalid. (The “default” or “coercion” element must be in the dictionary of the list element of the “args.cli” element. rule={rule})')
429
- if len([rk for rk in rule['rule'] if rk not in args_dict or rule['rule'][rk] != args_dict[rk]]) > 0:
430
- continue
431
- if 'default' in rule and rule['default'] is not None:
432
- for dk, dv in rule['default'].items():
433
- if dk not in args_dict or args_dict[dk] is None:
434
- if type(dv) == list:
435
- args_dict[dk] = [_cast(self, dk, v) for v in dv]
436
- else:
437
- args_dict[dk] = _cast(self, dk, dv)
438
- if 'coercion' in rule and rule['coercion'] is not None:
439
- for ck, cv in rule['coercion'].items():
440
- if type(cv) == list:
441
- args_dict[ck] = [_cast(self, ck, v) for v in cv]
442
- else:
443
- args_dict[ck] = _cast(self, ck, cv)
444
-
445
- def load_features_aliases_cli(self, logger:logging.Logger):
446
- yml = self.features_yml_data
447
- if yml is None: return
448
- if self.aliases_loaded_cli: return
449
- if 'aliases' not in yml or 'cli' not in yml['aliases']:
450
- return
451
-
452
- opt_cmd = self._options["cmd"].copy()
453
- for rule in yml['aliases']['cli']:
454
- if type(rule) is not dict:
455
- raise Exception(f'features.yml is invalid. (The aliases.cli” element must be a list element must be a dictionary. rule={rule})')
456
- if 'source' not in rule:
457
- raise Exception(f'features.yml is invalid. (The source element must be in the dictionary of the list element of the aliases.cli” element. rule={rule})')
458
- if 'target' not in rule:
459
- raise Exception(f'features.yml is invalid. (The target element must be in the dictionary of the list element of the aliases.cli” element. rule={rule})')
460
- if rule['source'] is None or rule['target'] is None:
461
- if logger.level == logging.DEBUG:
462
- logger.debug(f'Skip cli rule in features.yml. (The source or target element is None. rule={rule})')
463
- continue
464
- if type(rule['source']) is not dict:
465
- raise Exception(f'features.yml is invalid. (The aliases.cli.source” element must be a dictionary element must. rule={rule})')
466
- if type(rule['target']) is not dict:
467
- raise Exception(f'features.yml is invalid. (The aliases.cli.target element must be a dictionary element must. rule={rule})')
468
- if 'mode' not in rule['source'] or 'cmd' not in rule['source']:
469
- raise Exception(f'features.yml is invalid. (The aliases.cli.source element must have "mode" and "cmd" specified. rule={rule})')
470
- if 'mode' not in rule['target'] or 'cmd' not in rule['target']:
471
- raise Exception(f'features.yml is invalid. (The aliases.cli.target element must have "mode" and "cmd" specified. rule={rule})')
472
- if rule['source']['mode'] is None or rule['source']['cmd'] is None:
473
- if logger.level == logging.DEBUG:
474
- logger.debug(f'Skip cli rule in features.yml. (The source mode or cmd element is None. rule={rule})')
475
- continue
476
- if rule['target']['mode'] is None or rule['target']['cmd'] is None:
477
- if logger.level == logging.DEBUG:
478
- logger.debug(f'Skip cli rule in features.yml. (The target mode or cmd element is None. rule={rule})')
479
- continue
480
- tgt_move = True if 'move' in rule['target'] and rule['target']['move'] else False
481
- reg_src_cmd = re.compile(rule['source']['cmd'])
482
- for mk, mv in opt_cmd.items():
483
- if type(mv) is not dict: continue
484
- if mk != rule['source']['mode']: continue
485
- src_mode = mk
486
- tgt_mode = rule['target']['mode']
487
- self._options["cmd"][tgt_mode] = dict() if tgt_mode not in self._options["cmd"] else self._options["cmd"][tgt_mode]
488
- self._options["mode"][tgt_mode] = dict() if tgt_mode not in self._options["mode"] else self._options["mode"][tgt_mode]
489
- find = False
490
- for ck, cv in mv.copy().items():
491
- if type(cv) is not dict: continue
492
- ck_match:re.Match = reg_src_cmd.search(ck)
493
- if ck_match is None: continue
494
- find = True
495
- src_cmd = ck
496
- tgt_cmd = rule['target']['cmd'].format(*([ck_match.string]+list(ck_match.groups())))
497
- cv = cv.copy()
498
- cv['opt'] = tgt_cmd
499
- # cmd/[target mode]/[target cmd]に追加
500
- self._options["cmd"][tgt_mode][tgt_cmd] = cv
501
- # mode/[target mode]/[target cmd]に追加
502
- self._options["mode"][tgt_mode][tgt_cmd] = cv
503
- # mode/choiceにtarget modeがない場合は追加
504
- found_mode_choice = False
505
- for i, me in enumerate(self._options["mode"]["choice"]):
506
- if me['opt'] == tgt_mode:
507
- me[tgt_cmd] = cv.copy()
508
- found_mode_choice = True
509
- # 移動の場合は元を削除
510
- if tgt_move and me['opt'] == src_mode and src_cmd in me:
511
- del me[src_cmd]
512
- if not found_mode_choice:
513
- self._options["mode"]["choice"].append({'opt':tgt_mode, tgt_cmd:cv})
514
- # cmd/choiceにtarget cmdがない場合は追加
515
- found_cmd_choice = False
516
- for i, ce in enumerate(self._options["cmd"]["choice"]):
517
- if ce['opt'] == tgt_cmd:
518
- self._options["cmd"]["choice"][i] = cv
519
- found_cmd_choice = True
520
- # 移動の場合は元を削除(この処理をするとモード違いの同名コマンドが使えなくなるのでコメントアウト)
521
- #if tgt_move and ce['opt'] == src_cmd:
522
- # self._options["cmd"]["choice"].remove(ce)
523
- if not found_cmd_choice:
524
- self._options["cmd"]["choice"].append(cv)
525
- # 移動の場合は元を削除
526
- if tgt_move:
527
- if logger.level == logging.DEBUG:
528
- logger.debug(f'move command: src=({src_mode},{src_cmd}) -> tgt=({tgt_mode},{tgt_cmd})')
529
- if src_cmd in self._options["cmd"][src_mode]:
530
- del self._options["cmd"][src_mode][src_cmd]
531
- else:
532
- if logger.level == logging.DEBUG:
533
- logger.debug(f'copy command: src=({src_mode},{src_cmd}) -> tgt=({tgt_mode},{tgt_cmd})')
534
- if not find:
535
- logger.warning(f'Skip cli rule in features.yml. (Command matching the rule not found. rule={rule})')
536
- if len(self._options["cmd"][src_mode]) == 1:
537
- del self._options["cmd"][src_mode]
538
- if len(self._options["mode"][src_mode]) == 1:
539
- del self._options["mode"][src_mode]
540
- self.aliases_loaded_cli = True
541
-
542
- def load_features_aliases_web(self, routes:List[Route], logger:logging.Logger):
543
- yml = self.features_yml_data
544
- if yml is None: return
545
- if self.aliases_loaded_web: return
546
- if routes is None or type(routes) is not list or len(routes) == 0:
547
- raise Exception(f'routes is invalid. (The routes must be a list element.) routes={routes}')
548
- if 'aliases' not in yml or 'web' not in yml['aliases']:
549
- return
550
-
551
- for rule in yml['aliases']['web']:
552
- if type(rule) is not dict:
553
- raise Exception(f'features.yml is invalid. (The aliases.web element must be a list element must be a dictionary. rule={rule})')
554
- if 'source' not in rule:
555
- raise Exception(f'features.yml is invalid. (The source element must be in the dictionary of the list element of the aliases.web element. rule={rule})')
556
- if 'target' not in rule:
557
- raise Exception(f'features.yml is invalid. (The target element must be in the dictionary of the list element of the aliases.web element. rule={rule})')
558
- if rule['source'] is None or rule['target'] is None:
559
- if logger.level == logging.DEBUG:
560
- logger.debug(f'Skip web rule in features.yml. (The source or target element is None. rule={rule})')
561
- continue
562
- if type(rule['source']) is not dict:
563
- raise Exception(f'features.yml is invalid. (The aliases.web.source” element must be a dictionary element must. rule={rule})')
564
- if type(rule['target']) is not dict:
565
- raise Exception(f'features.yml is invalid. (The aliases.web.target element must be a dictionary element must. rule={rule})')
566
- if 'path' not in rule['source']:
567
- raise Exception(f'features.yml is invalid. (The aliases.web.source element must have "path" specified. rule={rule})')
568
- if 'path' not in rule['target']:
569
- raise Exception(f'features.yml is invalid. (The aliases.web.target element must have "path" specified. rule={rule})')
570
- if rule['source']['path'] is None:
571
- if logger.level == logging.DEBUG:
572
- logger.debug(f'Skip web rule in features.yml. (The source path element is None. rule={rule})')
573
- continue
574
- if rule['target']['path'] is None:
575
- if logger.level == logging.DEBUG:
576
- logger.debug(f'Skip web rule in features.yml. (The target path element is None. rule={rule})')
577
- continue
578
- tgt_move = True if 'move' in rule['target'] and rule['target']['move'] else False
579
- reg_src_path = re.compile(rule['source']['path'])
580
- find = False
581
- for route in routes.copy():
582
- if not isinstance(route, APIRoute):
583
- continue
584
- route_path = route.path
585
- path_match:re.Match = reg_src_path.search(route_path)
586
- if path_match is None: continue
587
- find = True
588
- tgt_Path = rule['target']['path'].format(*([path_match.string]+list(path_match.groups())))
589
- tgt_route = APIRoute(tgt_Path, route.endpoint, methods=route.methods, name=route.name,
590
- include_in_schema=route.include_in_schema)
591
- routes.append(tgt_route)
592
- if tgt_move:
593
- if logger.level == logging.DEBUG:
594
- logger.debug(f'move route: src=({route_path}) -> tgt=({tgt_Path})')
595
- routes.remove(route)
596
- else:
597
- if logger.level == logging.DEBUG:
598
- logger.debug(f'copy route: src=({route_path}) -> tgt=({tgt_Path})')
599
- if not find:
600
- logger.warning(f'Skip web rule in features.yml. (Command matching the rule not found. rule={rule})')
601
- self.aliases_loaded_web = True
1
+ from cmdbox.app import common, feature, web
2
+ from cmdbox.app.commons import module
3
+ from fastapi import Request
4
+ from fastapi.routing import APIRoute
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from starlette.routing import Route
8
+ from typing import List, Dict, Any
9
+ import argparse
10
+ import functools
11
+ import locale
12
+ import logging
13
+ import re
14
+ import time
15
+ import uuid
16
+
17
+
18
+ class Options:
19
+ T_INT = 'int'
20
+ T_FLOAT = 'float'
21
+ T_BOOL = 'bool'
22
+ T_STR = 'str'
23
+ T_DATE = 'date'
24
+ T_DATETIME = 'datetime'
25
+ T_DICT = 'dict'
26
+ T_TEXT = 'text'
27
+ T_FILE = 'file'
28
+ T_DIR = 'dir'
29
+
30
+ def __setattr__(self, name:str, value):
31
+ if name.startswith("T_") and name in self.__dict__:
32
+ raise ValueError(f'Cannot set attribute. ({name})')
33
+ self.__dict__[name] = value
34
+
35
+ _instance = None
36
+
37
+ @staticmethod
38
+ def getInstance(appcls=None, ver=None):
39
+ if Options._instance is None:
40
+ Options._instance = Options(appcls=appcls, ver=ver)
41
+ return Options._instance
42
+
43
+ def __init__(self, appcls=None, ver=None):
44
+ self.appcls = appcls
45
+ self.ver = ver
46
+ self.default_logger = common.default_logger(False, ver=self.ver, webcall=True)
47
+ self.features_yml_data = None
48
+ self.features_loaded = dict()
49
+ self.aliases_loaded_cli = False
50
+ self.aliases_loaded_web = False
51
+ self.audit_loaded = False
52
+ self.init_options()
53
+
54
+ def get_mode_keys(self) -> List[str]:
55
+ return [key for key,val in self._options["mode"].items() if type(val) == dict]
56
+
57
+ def get_modes(self) -> List[Dict[str, str]]:
58
+ """
59
+ 起動モードの選択肢を取得します。
60
+ Returns:
61
+ List[Dict[str, str]]: 起動モードの選択肢
62
+ """
63
+ return [''] + [{key:val} for key,val in self._options["mode"].items() if type(val) == dict]
64
+
65
+ def get_cmd_keys(self, mode:str) -> List[str]:
66
+ if mode not in self._options["cmd"]:
67
+ return []
68
+ return [key for key,val in self._options["cmd"][mode].items() if type(val) == dict]
69
+
70
+ def get_cmds(self, mode:str) -> List[Dict[str, str]]:
71
+ """
72
+ コマンドの選択肢を取得します。
73
+ Args:
74
+ mode: 起動モード
75
+ Returns:
76
+ List[Dict[str, str]]: コマンドの選択肢
77
+ """
78
+ if mode not in self._options["cmd"]:
79
+ return ['Please select mode.']
80
+ ret = [{key:val} for key,val in self._options["cmd"][mode].items() if type(val) == dict]
81
+ if len(ret) > 0:
82
+ return [''] + ret
83
+ return ['Please select mode.']
84
+
85
+ def get_cmd_attr(self, mode:str, cmd:str, attr:str) -> Any:
86
+ """
87
+ コマンドの属性を取得します。
88
+ Args:
89
+ mode: 起動モード
90
+ cmd: コマンド
91
+ attr: 属性
92
+ Returns:
93
+ Any: 属性の値
94
+ """
95
+ if mode not in self._options["cmd"]:
96
+ return [f'Unknown mode. ({mode})']
97
+ if cmd is None or cmd == "" or cmd not in self._options["cmd"][mode]:
98
+ return []
99
+ if attr not in self._options["cmd"][mode][cmd]:
100
+ return None
101
+ return self._options["cmd"][mode][cmd][attr]
102
+
103
+ def get_svcmd_feature(self, svcmd:str) -> Any:
104
+ """
105
+ サーバー側のコマンドのフューチャーを取得します。
106
+
107
+ Args:
108
+ svcmd: サーバー側のコマンド
109
+ Returns:
110
+ feature.Feature: フューチャー
111
+ """
112
+ if svcmd is None or svcmd == "":
113
+ return None
114
+ if svcmd not in self._options["svcmd"]:
115
+ return None
116
+ return self._options["svcmd"][svcmd]
117
+
118
+ def get_cmd_choices(self, mode:str, cmd:str, webmode:bool=False) -> List[Dict[str, Any]]:
119
+ """
120
+ コマンドのオプション一覧を取得します。
121
+ Args:
122
+ mode: 起動モード
123
+ cmd: コマンド
124
+ webmode (bool, optional): Webモードからの呼び出し. Defaults to False
125
+ Returns:
126
+ List[Dict[str, Any]]: オプションの選択肢
127
+ """
128
+ opts = self.get_cmd_attr(mode, cmd, "choice")
129
+ ret = []
130
+ for o in opts:
131
+ if not webmode or type(o) is not dict:
132
+ ret.append(o)
133
+ continue
134
+ o = o.copy()
135
+ if 'web' in o and o['web'] == 'mask':
136
+ o['default'] = '********'
137
+ ret.append(o)
138
+ return ret
139
+
140
+ def get_cmd_opt(self, mode:str, cmd:str, opt:str, webmode:bool=False) -> Dict[str, Any]:
141
+ """
142
+ コマンドのオプションを取得します。
143
+ Args:
144
+ mode: 起動モード
145
+ cmd: コマンド
146
+ opt: オプション
147
+ webmode (bool, optional): Webモードからの呼び出し. Defaults to False
148
+ Returns:
149
+ Dict[str, Any]: オプションの値
150
+ """
151
+ opts = self.get_cmd_choices(mode, cmd, webmode)
152
+ for o in opts:
153
+ if 'opt' in o and o['opt'] == opt:
154
+ return o
155
+ return None
156
+
157
+ def list_options(self):
158
+ def _list(ret, key, val):
159
+ if type(val) != dict or 'type' not in val:
160
+ return
161
+ opt = dict()
162
+ if val['type'] == Options.T_INT:
163
+ opt['type'] = int
164
+ opt['action'] = 'append' if val['multi'] else None
165
+ elif val['type'] == Options.T_FLOAT:
166
+ opt['type'] = float
167
+ opt['action'] = 'append' if val['multi'] else None
168
+ elif val['type'] == Options.T_BOOL:
169
+ opt['type'] = bool
170
+ opt['action'] = 'store_true'
171
+ elif val['type'] == Options.T_DICT:
172
+ opt['type'] = dict
173
+ if not val['multi']:
174
+ raise ValueError(f'list_options: The multi must be True if type is dict. key={key}, val={val}')
175
+ opt['action'] = 'append'
176
+ else:
177
+ opt['type'] = str
178
+ opt['action'] = 'append' if val['multi'] else None
179
+ o = [f'-{val["short"]}'] if "short" in val else []
180
+ o += [f'--{key}']
181
+ language, _ = locale.getlocale()
182
+ opt['help'] = val['discription_en'] if language.find('Japan') < 0 and language.find('ja_JP') < 0 else val['discription_ja']
183
+ opt['default'] = val['default']
184
+ if val['multi'] and val['default'] is not None:
185
+ raise ValueError(f'list_options: The default value must be None if multi is True. key={key}, val={val}')
186
+ opt['opts'] = o
187
+ if val['choice'] is not None:
188
+ opt['choices'] = []
189
+ for c in val['choice']:
190
+ if type(c) == dict:
191
+ opt['choices'] += [c['opt']]
192
+ elif c is not None and c != "":
193
+ opt['choices'] += [c]
194
+ else:
195
+ opt['choices'] = None
196
+ ret[key] = opt
197
+ ret = dict()
198
+ for k, v in self._options.items():
199
+ _list(ret, k, v)
200
+ #for mode in self._options["mode"]['choice']:
201
+ for _, cmd in self._options["cmd"].items():
202
+ if type(cmd) is not dict:
203
+ continue
204
+ for _, opt in cmd.items():
205
+ if type(opt) is not dict:
206
+ continue
207
+ for o in opt["choice"]:
208
+ if type(o) is not dict:
209
+ continue
210
+ _list(ret, o['opt'], o)
211
+ return ret
212
+
213
+ def mk_opt_list(self, opt:dict, webmode:bool=False) -> List[str]:
214
+ opt_schema = self.get_cmd_choices(opt['mode'], opt['cmd'], webmode)
215
+ opt_list = ['-m', opt['mode'], '-c', opt['cmd']]
216
+ file_dict = dict()
217
+ for key, val in opt.items():
218
+ if key in ['stdout_log', 'capture_stdout']:
219
+ continue
220
+ schema = [schema for schema in opt_schema if type(schema) is dict and schema['opt'] == key]
221
+ if len(schema) == 0 or val == '':
222
+ continue
223
+ if schema[0]['type'] == Options.T_BOOL:
224
+ if val:
225
+ opt_list.append(f"--{key}")
226
+ continue
227
+ if type(val) == list:
228
+ for v in val:
229
+ if v is None or v == '':
230
+ continue
231
+ opt_list.append(f"--{key}")
232
+ if str(v).find(' ') >= 0:
233
+ opt_list.append(f'"{v}"')
234
+ else:
235
+ opt_list.append(str(v))
236
+ elif type(val) == dict:
237
+ for k,v in val.items():
238
+ if k is None or k == '' or v is None or v == '':
239
+ continue
240
+ opt_list.append(f"--{key}")
241
+ k = f'"{k}"' if str(k).find(' ') >= 0 else str(k)
242
+ v = f'"{v}"' if str(v).find(' ') >= 0 else str(v)
243
+ opt_list.append(f'{k}={v}')
244
+ elif val is not None and val != '':
245
+ opt_list.append(f"--{key}")
246
+ if str(val).find(' ') >= 0:
247
+ opt_list.append(f'"{val}"')
248
+ else:
249
+ opt_list.append(str(val))
250
+ if 'fileio' in schema[0] and schema[0]['fileio'] == 'in' and type(val) != str:
251
+ file_dict[key] = val
252
+ return opt_list, file_dict
253
+
254
+ def init_options(self):
255
+ self._options = dict()
256
+ self._options["version"] = dict(
257
+ short="v", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=None,
258
+ discription_ja="バージョン表示",
259
+ discription_en="Display version")
260
+ self._options["useopt"] = dict(
261
+ short="u", type=Options.T_STR, default=None, required=False, multi=False, hide=True, choice=None,
262
+ discription_ja="オプションを保存しているファイルを使用します。",
263
+ discription_en="Use the file that saves the options.")
264
+ self._options["saveopt"] = dict(
265
+ short="s", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=[True, False],
266
+ discription_ja="指定しているオプションを `-u` で指定したファイルに保存します。",
267
+ discription_en="Save the specified options to the file specified by `-u`.")
268
+ self._options["debug"] = dict(
269
+ short="d", type=Options.T_BOOL, default=False, required=False, multi=False, hide=True, choice=[True, False],
270
+ discription_ja="デバックモードで起動します。",
271
+ discription_en="Starts in debug mode.")
272
+ self._options["format"] = dict(
273
+ short="f", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True,
274
+ discription_ja="処理結果を見やすい形式で出力します。指定しない場合json形式で出力します。",
275
+ discription_en="Output the processing result in an easy-to-read format. If not specified, output in json format.",
276
+ choice=None)
277
+ self._options["mode"] = dict(
278
+ short="m", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
279
+ discription_ja="起動モードを指定します。",
280
+ discription_en="Specify the startup mode.",
281
+ choice=[])
282
+ self._options["cmd"] = dict(
283
+ short="c", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
284
+ discription_ja="コマンドを指定します。",
285
+ discription_en="Specify the command.",
286
+ choice=[])
287
+ self._options["tag"] = dict(
288
+ short="t", type=Options.T_STR, default=None, required=False, multi=True, hide=True,
289
+ discription_ja="このコマンドのタグを指定します。",
290
+ discription_en="Specify the tag for this command.",
291
+ choice=None)
292
+ self._options["clmsg_id"] = dict(
293
+ type=Options.T_STR, default=None, required=False, multi=False, hide=True,
294
+ discription_ja="クライアントのメッセージIDを指定します。省略した場合はuuid4で生成されます。",
295
+ discription_en="Specifies the message ID of the client. If omitted, uuid4 will be generated.",
296
+ choice=None)
297
+
298
+ def init_debugoption(self):
299
+ # デバックオプションを追加
300
+ self._options["debug"]["opt"] = "debug"
301
+ self._options["tag"]["opt"] = "tag"
302
+ self._options["clmsg_id"]["opt"] = "clmsg_id"
303
+ for key, mode in self._options["cmd"].items():
304
+ if type(mode) is not dict:
305
+ continue
306
+ mode['opt'] = key
307
+ for k, c in mode.items():
308
+ if type(c) is not dict:
309
+ continue
310
+ c["opt"] = k
311
+ if "debug" not in [_o['opt'] for _o in c["choice"]]:
312
+ c["choice"].append(self._options["debug"])
313
+ if "tag" not in [_o['opt'] for _o in c["choice"]]:
314
+ c["choice"].append(self._options["tag"])
315
+ if "clmsg_id" not in [_o['opt'] for _o in c["choice"]]:
316
+ c["choice"].append(self._options["clmsg_id"])
317
+ if c["opt"] not in [_o['opt'] for _o in self._options["cmd"]["choice"]]:
318
+ self._options["cmd"]["choice"] += [c]
319
+ self._options["mode"][key] = mode
320
+ self._options["mode"]["choice"] += [mode]
321
+
322
+ def load_svcmd(self, package_name:str, prefix:str="cmdbox_", excludes:list=[], appcls=None, ver=None, logger:logging.Logger=None, isloaded:bool=True):
323
+ """
324
+ 指定されたパッケージの指定された接頭語を持つモジュールを読み込みます。
325
+
326
+ Args:
327
+ package_name (str): パッケージ名
328
+ prefix (str): 接頭語
329
+ excludes (list): 除外するモジュール名のリスト
330
+ appcls (Any): アプリケーションクラス
331
+ ver (Any): バージョンモジュール
332
+ logger (logging.Logger): ロガー
333
+ isloaded (bool): 読み込み済みかどうか
334
+ """
335
+ if "svcmd" not in self._options:
336
+ self._options["svcmd"] = dict()
337
+ for mode, f in module.load_features(package_name, prefix, excludes, appcls=appcls, ver=ver).items():
338
+ if mode not in self._options["cmd"]:
339
+ self._options["cmd"][mode] = dict()
340
+ for cmd, opt in f.items():
341
+ self._options["cmd"][mode][cmd] = opt
342
+ fobj:feature.Feature = opt['feature']
343
+ if not isloaded and logger is not None and logger.level == logging.DEBUG:
344
+ logger.debug(f"loaded features: mode={mode}, cmd={cmd}, {fobj}")
345
+ svcmd = fobj.get_svcmd()
346
+ if svcmd is not None:
347
+ self._options["svcmd"][svcmd] = fobj
348
+ self.init_debugoption()
349
+
350
+ def is_features_loaded(self, ftype:str) -> bool:
351
+ """
352
+ 指定されたフィーチャータイプが読み込まれているかどうかを返します。
353
+
354
+ Args:
355
+ ftype (str): フィーチャータイプ
356
+ Returns:
357
+ bool: 読み込まれているかどうか
358
+ """
359
+ return ftype in self.features_loaded and self.features_loaded[ftype]
360
+
361
+ def load_features_file(self, ftype:str, func, appcls, ver, logger:logging.Logger=None):
362
+ """
363
+ フィーチャーファイル(features.yml)を読み込みます。
364
+
365
+ Args:
366
+ ftype (str): フィーチャータイプ。cli又はweb
367
+ func (Any): フィーチャーの処理関数
368
+ appcls (Any): アプリケーションクラス
369
+ ver (Any): バージョンモジュール
370
+ logger (logging.Logger): ロガー
371
+ """
372
+ # 読込み済みかどうかの判定
373
+ if self.is_features_loaded(ftype):
374
+ return
375
+ # cmdboxを拡張したアプリをカスタマイズするときのfeatures.ymlを読み込む
376
+ features_yml = Path(f'.{ver.__appid__}/features.yml')
377
+ if not features_yml.exists() or not features_yml.is_file():
378
+ # cmdboxを拡張したアプリの組み込みfeatures.ymlを読み込む
379
+ features_yml = Path(ver.__file__).parent / 'extensions' / 'features.yml'
380
+ #if not features_yml.exists() or not features_yml.is_file():
381
+ # features_yml = Path('.samples/features.yml')
382
+ logger.info(f"load features.yml: {features_yml}, is_file={features_yml.is_file()}")
383
+ if features_yml.exists() and features_yml.is_file():
384
+ if self.features_yml_data is None:
385
+ self.features_yml_data = yml = common.load_yml(features_yml)
386
+ if logger.level == logging.DEBUG:
387
+ logger.debug(f"features.yml data: {yml}")
388
+ else:
389
+ yml = self.features_yml_data
390
+ if yml is None: return
391
+ if 'features' not in yml:
392
+ raise Exception('features.yml is invalid. (The root element must be "features".)')
393
+ if ftype not in yml['features']:
394
+ raise Exception(f'features.yml is invalid. (There is no {ftype}in the “features” element.)')
395
+ if yml['features'][ftype] is None:
396
+ return
397
+ if type(yml['features'][ftype]) is not list:
398
+ raise Exception(f'features.yml is invalid. (The “features.{ftype} element must be a list. {ftype}={yml["features"][ftype]})')
399
+ for data in yml['features'][ftype]:
400
+ if type(data) is not dict:
401
+ raise Exception(f'features.yml is invalid. (The “features.{ftype}” element must be a list element must be a dictionary. data={data})')
402
+ if 'package' not in data:
403
+ raise Exception(f'features.yml is invalid. (The “package” element must be in the dictionary of the list element of the “features.{ftype}” element. data={data})')
404
+ if 'prefix' not in data:
405
+ raise Exception(f'features.yml is invalid. (The prefix element must be in the dictionary of the list element of the “features.{ftype}” element. data={data})')
406
+ if data['package'] is None or data['package'] == "":
407
+ continue
408
+ if data['prefix'] is None or data['prefix'] == "":
409
+ continue
410
+ exclude_modules = []
411
+ if 'exclude_modules' in data:
412
+ if type(data['exclude_modules']) is not list:
413
+ raise Exception(f'features.yml is invalid. (The “exclude_modules” element must be a list element. data={data})')
414
+ exclude_modules = data['exclude_modules']
415
+ func(data['package'], data['prefix'], exclude_modules, appcls, ver, logger, self.is_features_loaded(ftype))
416
+ self.features_loaded[ftype] = True
417
+
418
+ def load_features_args(self, args_dict:Dict[str, Any]):
419
+ yml = self.features_yml_data
420
+ if yml is None:
421
+ return
422
+ if 'args' not in yml or 'cli' not in yml['args']:
423
+ return
424
+
425
+ opts = self.list_options()
426
+ def _cast(self, key, val):
427
+ for opt in opts.values():
428
+ if f"--{key}" in opt['opts']:
429
+ if opt['type'] == int:
430
+ return int(val)
431
+ elif opt['type'] == float:
432
+ return float(val)
433
+ elif opt['type'] == bool:
434
+ return True
435
+ else:
436
+ return eval(val)
437
+ return None
438
+
439
+ for rule in yml['args']['cli']:
440
+ if type(rule) is not dict:
441
+ raise Exception(f'features.yml is invalid. (The “args.cli” element must be a list element must be a dictionary. rule={rule})')
442
+ if 'rule' not in rule:
443
+ raise Exception(f'features.yml is invalid. (The “rule” element must be in the dictionary of the list element of the “args.cli” element. rule={rule})')
444
+ if rule['rule'] is None:
445
+ continue
446
+ if 'default' not in rule and 'coercion' not in rule:
447
+ raise Exception(f'features.yml is invalid. (The “default” or “coercion” element must be in the dictionary of the list element of the “args.cli” element. rule={rule})')
448
+ if len([rk for rk in rule['rule'] if rk not in args_dict or rule['rule'][rk] != args_dict[rk]]) > 0:
449
+ continue
450
+ if 'default' in rule and rule['default'] is not None:
451
+ for dk, dv in rule['default'].items():
452
+ if dk not in args_dict or args_dict[dk] is None:
453
+ if type(dv) == list:
454
+ args_dict[dk] = [_cast(self, dk, v) for v in dv]
455
+ else:
456
+ args_dict[dk] = _cast(self, dk, dv)
457
+ if 'coercion' in rule and rule['coercion'] is not None:
458
+ for ck, cv in rule['coercion'].items():
459
+ if type(cv) == list:
460
+ args_dict[ck] = [_cast(self, ck, v) for v in cv]
461
+ else:
462
+ args_dict[ck] = _cast(self, ck, cv)
463
+
464
+ def load_features_aliases_cli(self, logger:logging.Logger):
465
+ yml = self.features_yml_data
466
+ if yml is None: return
467
+ if self.aliases_loaded_cli: return
468
+ if 'aliases' not in yml or 'cli' not in yml['aliases']:
469
+ return
470
+
471
+ opt_cmd = self._options["cmd"].copy()
472
+ for rule in yml['aliases']['cli']:
473
+ if type(rule) is not dict:
474
+ raise Exception(f'features.yml is invalid. (The aliases.cli” element must be a list element must be a dictionary. rule={rule})')
475
+ if 'source' not in rule:
476
+ raise Exception(f'features.yml is invalid. (The source element must be in the dictionary of the list element of the aliases.cli” element. rule={rule})')
477
+ if 'target' not in rule:
478
+ raise Exception(f'features.yml is invalid. (The target element must be in the dictionary of the list element of the aliases.cli” element. rule={rule})')
479
+ if rule['source'] is None or rule['target'] is None:
480
+ if logger.level == logging.DEBUG:
481
+ logger.debug(f'Skip cli rule in features.yml. (The source or target element is None. rule={rule})')
482
+ continue
483
+ if type(rule['source']) is not dict:
484
+ raise Exception(f'features.yml is invalid. (The aliases.cli.source” element must be a dictionary element must. rule={rule})')
485
+ if type(rule['target']) is not dict:
486
+ raise Exception(f'features.yml is invalid. (The aliases.cli.target element must be a dictionary element must. rule={rule})')
487
+ if 'mode' not in rule['source'] or 'cmd' not in rule['source']:
488
+ raise Exception(f'features.yml is invalid. (The aliases.cli.source element must have "mode" and "cmd" specified. rule={rule})')
489
+ if 'mode' not in rule['target'] or 'cmd' not in rule['target']:
490
+ raise Exception(f'features.yml is invalid. (The aliases.cli.target element must have "mode" and "cmd" specified. rule={rule})')
491
+ if rule['source']['mode'] is None or rule['source']['cmd'] is None:
492
+ if logger.level == logging.DEBUG:
493
+ logger.debug(f'Skip cli rule in features.yml. (The source mode or cmd element is None. rule={rule})')
494
+ continue
495
+ if rule['target']['mode'] is None or rule['target']['cmd'] is None:
496
+ if logger.level == logging.DEBUG:
497
+ logger.debug(f'Skip cli rule in features.yml. (The target mode or cmd element is None. rule={rule})')
498
+ continue
499
+ tgt_move = True if 'move' in rule['target'] and rule['target']['move'] else False
500
+ reg_src_cmd = re.compile(rule['source']['cmd'])
501
+ for mk, mv in opt_cmd.items():
502
+ if type(mv) is not dict: continue
503
+ if mk != rule['source']['mode']: continue
504
+ src_mode = mk
505
+ tgt_mode = rule['target']['mode']
506
+ self._options["cmd"][tgt_mode] = dict() if tgt_mode not in self._options["cmd"] else self._options["cmd"][tgt_mode]
507
+ self._options["mode"][tgt_mode] = dict() if tgt_mode not in self._options["mode"] else self._options["mode"][tgt_mode]
508
+ find = False
509
+ for ck, cv in mv.copy().items():
510
+ if type(cv) is not dict: continue
511
+ ck_match:re.Match = reg_src_cmd.search(ck)
512
+ if ck_match is None: continue
513
+ find = True
514
+ src_cmd = ck
515
+ tgt_cmd = rule['target']['cmd'].format(*([ck_match.string]+list(ck_match.groups())))
516
+ cv = cv.copy()
517
+ cv['opt'] = tgt_cmd
518
+ # cmd/[target mode]/[target cmd]に追加
519
+ self._options["cmd"][tgt_mode][tgt_cmd] = cv
520
+ # mode/[target mode]/[target cmd]に追加
521
+ self._options["mode"][tgt_mode][tgt_cmd] = cv
522
+ # mode/choiceにtarget modeがない場合は追加
523
+ found_mode_choice = False
524
+ for i, me in enumerate(self._options["mode"]["choice"]):
525
+ if me['opt'] == tgt_mode:
526
+ me[tgt_cmd] = cv.copy()
527
+ found_mode_choice = True
528
+ # 移動の場合は元を削除
529
+ if tgt_move and me['opt'] == src_mode and src_cmd in me:
530
+ del me[src_cmd]
531
+ if not found_mode_choice:
532
+ self._options["mode"]["choice"].append({'opt':tgt_mode, tgt_cmd:cv})
533
+ # cmd/choiceにtarget cmdがない場合は追加
534
+ found_cmd_choice = False
535
+ for i, ce in enumerate(self._options["cmd"]["choice"]):
536
+ if ce['opt'] == tgt_cmd:
537
+ self._options["cmd"]["choice"][i] = cv
538
+ found_cmd_choice = True
539
+ # 移動の場合は元を削除(この処理をするとモード違いの同名コマンドが使えなくなるのでコメントアウト)
540
+ #if tgt_move and ce['opt'] == src_cmd:
541
+ # self._options["cmd"]["choice"].remove(ce)
542
+ if not found_cmd_choice:
543
+ self._options["cmd"]["choice"].append(cv)
544
+ # 移動の場合は元を削除
545
+ if tgt_move:
546
+ if logger.level == logging.DEBUG:
547
+ logger.debug(f'move command: src=({src_mode},{src_cmd}) -> tgt=({tgt_mode},{tgt_cmd})')
548
+ if src_cmd in self._options["cmd"][src_mode]:
549
+ del self._options["cmd"][src_mode][src_cmd]
550
+ else:
551
+ if logger.level == logging.DEBUG:
552
+ logger.debug(f'copy command: src=({src_mode},{src_cmd}) -> tgt=({tgt_mode},{tgt_cmd})')
553
+ if not find:
554
+ logger.warning(f'Skip cli rule in features.yml. (Command matching the rule not found. rule={rule})')
555
+ if len(self._options["cmd"][src_mode]) == 1:
556
+ del self._options["cmd"][src_mode]
557
+ if len(self._options["mode"][src_mode]) == 1:
558
+ del self._options["mode"][src_mode]
559
+ self.aliases_loaded_cli = True
560
+
561
+ def load_features_aliases_web(self, routes:List[Route], logger:logging.Logger):
562
+ yml = self.features_yml_data
563
+ if yml is None: return
564
+ if self.aliases_loaded_web: return
565
+ if routes is None or type(routes) is not list or len(routes) == 0:
566
+ raise Exception(f'routes is invalid. (The routes must be a list element.) routes={routes}')
567
+ if 'aliases' not in yml or 'web' not in yml['aliases']:
568
+ return
569
+
570
+ for rule in yml['aliases']['web']:
571
+ if type(rule) is not dict:
572
+ raise Exception(f'features.yml is invalid. (The aliases.web element must be a list element must be a dictionary. rule={rule})')
573
+ if 'source' not in rule:
574
+ raise Exception(f'features.yml is invalid. (The source element must be in the dictionary of the list element of the aliases.web element. rule={rule})')
575
+ if 'target' not in rule:
576
+ raise Exception(f'features.yml is invalid. (The target element must be in the dictionary of the list element of the aliases.web element. rule={rule})')
577
+ if rule['source'] is None or rule['target'] is None:
578
+ if logger.level == logging.DEBUG:
579
+ logger.debug(f'Skip web rule in features.yml. (The source or target element is None. rule={rule})')
580
+ continue
581
+ if type(rule['source']) is not dict:
582
+ raise Exception(f'features.yml is invalid. (The aliases.web.source” element must be a dictionary element must. rule={rule})')
583
+ if type(rule['target']) is not dict:
584
+ raise Exception(f'features.yml is invalid. (The aliases.web.target element must be a dictionary element must. rule={rule})')
585
+ if 'path' not in rule['source']:
586
+ raise Exception(f'features.yml is invalid. (The aliases.web.source element must have "path" specified. rule={rule})')
587
+ if 'path' not in rule['target']:
588
+ raise Exception(f'features.yml is invalid. (The aliases.web.target element must have "path" specified. rule={rule})')
589
+ if rule['source']['path'] is None:
590
+ if logger.level == logging.DEBUG:
591
+ logger.debug(f'Skip web rule in features.yml. (The source path element is None. rule={rule})')
592
+ continue
593
+ if rule['target']['path'] is None:
594
+ if logger.level == logging.DEBUG:
595
+ logger.debug(f'Skip web rule in features.yml. (The target path element is None. rule={rule})')
596
+ continue
597
+ tgt_move = True if 'move' in rule['target'] and rule['target']['move'] else False
598
+ reg_src_path = re.compile(rule['source']['path'])
599
+ find = False
600
+ for route in routes.copy():
601
+ if not isinstance(route, APIRoute):
602
+ continue
603
+ route_path = route.path
604
+ path_match:re.Match = reg_src_path.search(route_path)
605
+ if path_match is None: continue
606
+ find = True
607
+ tgt_Path = rule['target']['path'].format(*([path_match.string]+list(path_match.groups())))
608
+ tgt_route = APIRoute(tgt_Path, route.endpoint, methods=route.methods, name=route.name,
609
+ include_in_schema=route.include_in_schema)
610
+ routes.append(tgt_route)
611
+ if tgt_move:
612
+ if logger.level == logging.DEBUG:
613
+ logger.debug(f'move route: src=({route_path}) -> tgt=({tgt_Path})')
614
+ routes.remove(route)
615
+ else:
616
+ if logger.level == logging.DEBUG:
617
+ logger.debug(f'copy route: src=({route_path}) -> tgt=({tgt_Path})')
618
+ if not find:
619
+ logger.warning(f'Skip web rule in features.yml. (Command matching the rule not found. rule={rule})')
620
+ self.aliases_loaded_web = True
621
+
622
+ def load_features_audit(self, logger:logging.Logger):
623
+ yml = self.features_yml_data
624
+ if yml is None: return
625
+ if self.audit_loaded: return
626
+ if 'audit' not in yml: return
627
+ if 'enabled' not in yml['audit']:
628
+ raise Exception('features.yml is invalid. (The audit element must have "enabled" specified.)')
629
+ if not yml['audit']['enabled']: return
630
+ # writeフューチャー
631
+ if 'write' not in yml['audit']:
632
+ raise Exception('features.yml is invalid. (The audit element must have "write" specified.)')
633
+ if 'mode' not in yml['audit']['write']:
634
+ raise Exception('features.yml is invalid. (The audit.write element must have "mode" specified.)')
635
+ mode = yml['audit']['write']['mode']
636
+ if 'cmd' not in yml['audit']['write']:
637
+ raise Exception('features.yml is invalid. (The audit.write element must have "cmd" specified.)')
638
+ cmd = yml['audit']['write']['cmd']
639
+ self.audit_write:feature.Feature = self.get_cmd_attr(mode, cmd, 'feature')
640
+ # searchフューチャー
641
+ if 'search' not in yml['audit']:
642
+ raise Exception('features.yml is invalid. (The audit element must have "search" specified.)')
643
+ if 'mode' not in yml['audit']['search']:
644
+ raise Exception('features.yml is invalid. (The audit.search element must have "mode" specified.)')
645
+ mode = yml['audit']['search']['mode']
646
+ if 'cmd' not in yml['audit']['search']:
647
+ raise Exception('features.yml is invalid. (The audit.search element must have "cmd" specified.)')
648
+ cmd = yml['audit']['search']['cmd']
649
+ self.audit_search:feature.Feature = self.get_cmd_attr(mode, cmd, 'feature')
650
+ # フューチャーのoptions
651
+ if 'options' not in yml['audit']:
652
+ raise Exception('features.yml is invalid. (The audit element must have "options" specified.)')
653
+ self.audit_write_args = yml['audit']['options'].copy()
654
+ self.audit_write_args['mode'] = mode
655
+ self.audit_write_args['cmd'] = cmd
656
+ self.audit_search_args = yml['audit']['options'].copy()
657
+ self.audit_search_args['mode'] = mode
658
+ self.audit_search_args['cmd'] = cmd
659
+ self.audit_loaded = True
660
+
661
+ AT_USER = 'user'
662
+ AT_ADMIN = 'admin'
663
+ AT_SYSTEM = 'system'
664
+ AT_AUTH = 'auth'
665
+ AT_EVENT = 'event'
666
+ AUDITS = [AT_USER, AT_ADMIN, AT_SYSTEM, AT_AUTH, AT_EVENT]
667
+
668
+ @staticmethod
669
+ def audit(body:Dict[str, Any]=None, audit_type:str=None, tags:List[str]=None, src:str=None) -> int:
670
+ """
671
+ 監査ログを書き込む関数を返します。
672
+ デコレーターとして使用することができます。
673
+
674
+ Args:
675
+ body (Dict[str, Any]): 監査ログの内容
676
+ audit_type (str): 監査の種類
677
+ tags (List[str]): メッセージのタグ
678
+ src (str): メッセージの発生源
679
+
680
+ Returns:
681
+ int: レスポンスコード
682
+ """
683
+ self = Options.getInstance()
684
+ if body is None:
685
+ body = dict()
686
+ if body is not None and type(body) is not dict:
687
+ raise Exception('body is invalid. (The body must be a dictionary element.)')
688
+ if audit_type is not None and audit_type not in Options.AUDITS:
689
+ raise Exception(f'audit_type is invalid. (The audit_type must be one of the following: {Options.AUDITS})')
690
+ tags = tags if tags is not None else []
691
+ if tags is not None and type(tags) is not list:
692
+ raise Exception('clmsg_tags is invalid. (The clmsg_tags must be a list element.)')
693
+ def _audit_write(func):
694
+ @functools.wraps(func)
695
+ def _wrapper(*args, **kwargs):
696
+ self.audit_exec(*args, body=body, audit_type=audit_type, tags=tags, src=src, **kwargs)
697
+ ret = func(*args, **kwargs)
698
+ return ret
699
+ return _wrapper
700
+ return _audit_write
701
+
702
+ def audit_exec(self, *args, body:Dict[str, Any]=None, audit_type:str=None, tags:List[str]=None, src:str=None, title:str=None, user:str=None, **kwargs) -> None:
703
+ """
704
+ 監査ログを書き込みます。
705
+
706
+ Args:
707
+ args (Any): 呼び出し元で使用している引数
708
+ body (Dict[str, Any]): 監査ログの内容
709
+ audit_type (str): 監査の種類
710
+ tags (List[str]): メッセージのタグ
711
+ src (str): メッセージの発生源
712
+ title (str): メッセージのタイトル
713
+ user (str): メッセージを発生させたユーザー名
714
+ kwargs (Any): 呼び出し元で使用しているキーワード引数
715
+ """
716
+ if not hasattr(self, 'audit_write') or self.audit_write is None:
717
+ raise Exception('audit write feature is not found.')
718
+ clmsg_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + common.get_tzoffset_str()
719
+ opt = self.audit_write_args.copy()
720
+ opt['audit_type'] = audit_type
721
+ opt['clmsg_id'] = str(uuid.uuid4())
722
+ opt['clmsg_date'] = clmsg_date
723
+ opt['clmsg_src'] = opt['clmsg_src'] if 'clmsg_src' in opt else None
724
+ opt['clmsg_title'] = opt['clmsg_title'] if 'clmsg_title' in opt else None
725
+ opt['clmsg_user'] = user
726
+ opt['clmsg_tag'] = tags
727
+ opt['format'] = False if opt.get('format') is None else opt['format']
728
+ opt['output_json'] = None if opt.get('output_json') is None else opt['output_json']
729
+ opt['output_json_append'] = False if opt.get('output_json_append') is None else opt['output_json_append']
730
+ opt['host'] = 'localhost' if opt.get('host') is None else opt['host']
731
+ opt['port'] = 6379 if opt.get('port') is None else opt['port']
732
+ opt['password'] = 'password' if opt.get('password') is None else opt['password']
733
+ opt['svname'] = 'server' if opt.get('svname') is None else opt['svname']
734
+ opt['retry_count'] = 1 if opt.get('retry_count') is None else opt['retry_count']
735
+ opt['retry_interval'] = 1 if opt.get('retry_interval') is None else opt['retry_interval']
736
+ opt['timeout'] = 5 if opt.get('timeout') is None else opt['timeout']
737
+ opt['pg_enabled'] = False if opt.get('pg_enabled') is None else opt['pg_enabled']
738
+ opt['pg_host'] = 'localhost' if opt.get('pg_host') is None else opt['pg_host']
739
+ opt['pg_port'] = 5432 if opt.get('pg_port') is None else opt['pg_port']
740
+ opt['pg_user'] = 'postgres' if opt.get('pg_user') is None else opt['pg_user']
741
+ opt['pg_password'] = 'postgres' if opt.get('pg_password') is None else opt['pg_password']
742
+ opt['pg_dbname'] = 'audit' if opt.get('pg_dbname') is None else opt['pg_dbname']
743
+ logger = self.default_logger
744
+ clmsg_body = body.copy() if body is not None else dict()
745
+ func_feature = None
746
+ for arg in list(args) + list(kwargs.values()):
747
+ if isinstance(arg, logging.Logger): logger = arg
748
+ elif isinstance(arg, argparse.Namespace):
749
+ mode = arg.mode if hasattr(arg, 'mode') else None
750
+ cmd = arg.cmd if hasattr(arg, 'cmd') else None
751
+ if mode is not None and cmd is not None:
752
+ opt_schema = self.get_cmd_choices(mode, cmd, True)
753
+ for key, val in arg.__dict__.items():
754
+ if key in ['stdout_log', 'capture_stdout']:
755
+ continue
756
+ schema = [schema for schema in opt_schema if type(schema) is dict and schema['opt'] == key]
757
+ if len(schema) == 0 or val == '' or val is None:
758
+ continue
759
+ if 'web' in schema[0] and schema[0]['web'] == 'mask':
760
+ clmsg_body[key] = '********'
761
+ else:
762
+ clmsg_body[key] = common.to_str(val, 100)
763
+ opt[key] = val
764
+ if hasattr(arg, 'clmsg_id'): opt['clmsg_id'] = arg.clmsg_id
765
+ elif isinstance(arg, web.Web):
766
+ opt['host'] = arg.redis_host
767
+ opt['port'] = arg.redis_port
768
+ opt['password'] = arg.redis_password
769
+ opt['svname'] = arg.svname
770
+ elif isinstance(arg, feature.Feature):
771
+ func_feature = arg
772
+ opt['clmsg_src'] = func_feature.__class__.__name__
773
+ elif isinstance(arg, Request):
774
+ if 'signin' in arg.session and arg.session['signin'] is not None and 'name' in arg.session['signin']:
775
+ opt['clmsg_user'] = arg.session['signin']['name']
776
+ if opt['audit_type'] is None:
777
+ opt['audit_type'] = Options.AT_ADMIN if 'admin' in arg.session['signin']['groups'] else Options.AT_USER
778
+ opt['clmsg_id'] = arg.session['signin']['clmsg_id'] if 'clmsg_id' in arg.session['signin'] else opt['clmsg_id']
779
+ arg.session['signin']['clmsg_id'] = opt['clmsg_id']
780
+ opt['clmsg_src'] = arg.url.path
781
+ opt['clmsg_body'] = clmsg_body
782
+ opt['audit_type'] = opt['audit_type'] if opt['audit_type'] else Options.AT_EVENT
783
+ if src is not None and src != "":
784
+ opt['clmsg_src'] = src
785
+ if title is not None and title != "":
786
+ opt['clmsg_title'] = title
787
+ audit_write_args = argparse.Namespace(**{k:common.chopdq(v) for k,v in opt.items()})
788
+ self.audit_write.apprun(logger, audit_write_args, tm=0.0, pf=[])