cmdbox 0.6.3.2__py3-none-any.whl → 0.6.4.1__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 (168) hide show
  1. cmdbox/app/app.py +22 -7
  2. cmdbox/app/common.py +9 -6
  3. cmdbox/app/commons/loghandler.py +101 -20
  4. cmdbox/app/commons/redis_client.py +1 -1
  5. cmdbox/app/edge.py +2 -4
  6. cmdbox/app/feature.py +1 -1
  7. cmdbox/app/features/cli/cmdbox_agent_mcp_client.py +175 -0
  8. cmdbox/app/features/cli/cmdbox_agent_mcp_proxy.py +96 -0
  9. cmdbox/app/features/cli/cmdbox_audit_createdb.py +224 -224
  10. cmdbox/app/features/cli/cmdbox_audit_delete.py +4 -4
  11. cmdbox/app/features/cli/cmdbox_audit_search.py +4 -4
  12. cmdbox/app/features/cli/cmdbox_audit_write.py +6 -8
  13. cmdbox/app/features/cli/cmdbox_client_file_copy.py +3 -3
  14. cmdbox/app/features/cli/cmdbox_client_file_download.py +3 -3
  15. cmdbox/app/features/cli/cmdbox_client_file_list.py +3 -3
  16. cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +3 -3
  17. cmdbox/app/features/cli/cmdbox_client_file_move.py +3 -3
  18. cmdbox/app/features/cli/cmdbox_client_file_remove.py +3 -3
  19. cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +3 -3
  20. cmdbox/app/features/cli/cmdbox_client_file_upload.py +3 -3
  21. cmdbox/app/features/cli/cmdbox_client_http.py +7 -6
  22. cmdbox/app/features/cli/cmdbox_client_server_info.py +4 -4
  23. cmdbox/app/features/cli/cmdbox_cmd_list.py +4 -4
  24. cmdbox/app/features/cli/cmdbox_cmd_load.py +5 -5
  25. cmdbox/app/features/cli/cmdbox_edge_config.py +1 -1
  26. cmdbox/app/features/cli/cmdbox_edge_start.py +3 -3
  27. cmdbox/app/features/cli/cmdbox_mcp_client.py +174 -174
  28. cmdbox/app/features/cli/cmdbox_mcp_proxy.py +96 -96
  29. cmdbox/app/features/cli/cmdbox_server_list.py +3 -3
  30. cmdbox/app/features/cli/cmdbox_server_start.py +103 -103
  31. cmdbox/app/features/cli/cmdbox_server_stop.py +6 -6
  32. cmdbox/app/features/cli/cmdbox_tts_install.py +317 -0
  33. cmdbox/app/features/cli/cmdbox_tts_say.py +179 -0
  34. cmdbox/app/features/cli/cmdbox_tts_start.py +329 -0
  35. cmdbox/app/features/cli/cmdbox_tts_stop.py +108 -0
  36. cmdbox/app/features/cli/cmdbox_web_apikey_add.py +91 -91
  37. cmdbox/app/features/cli/cmdbox_web_apikey_del.py +91 -91
  38. cmdbox/app/features/cli/cmdbox_web_gencert.py +7 -7
  39. cmdbox/app/features/cli/cmdbox_web_genpass.py +168 -168
  40. cmdbox/app/features/cli/cmdbox_web_group_add.py +94 -94
  41. cmdbox/app/features/cli/cmdbox_web_group_del.py +87 -87
  42. cmdbox/app/features/cli/cmdbox_web_group_edit.py +94 -94
  43. cmdbox/app/features/cli/cmdbox_web_group_list.py +87 -87
  44. cmdbox/app/features/cli/cmdbox_web_start.py +236 -235
  45. cmdbox/app/features/cli/cmdbox_web_stop.py +72 -72
  46. cmdbox/app/features/cli/cmdbox_web_user_add.py +104 -104
  47. cmdbox/app/features/cli/cmdbox_web_user_del.py +87 -87
  48. cmdbox/app/features/cli/cmdbox_web_user_edit.py +104 -104
  49. cmdbox/app/features/cli/cmdbox_web_user_list.py +87 -87
  50. cmdbox/app/features/web/cmdbox_web_agent.py +16 -4
  51. cmdbox/app/features/web/cmdbox_web_get_cmd_choices.py +10 -2
  52. cmdbox/app/features/web/cmdbox_web_save_cmd.py +1 -0
  53. cmdbox/app/features/web/cmdbox_web_versions_used.py +4 -0
  54. cmdbox/app/filer.py +9 -9
  55. cmdbox/app/mcp.py +19 -8
  56. cmdbox/app/options.py +52 -47
  57. cmdbox/app/server.py +224 -224
  58. cmdbox/app/web.py +39 -17
  59. cmdbox/extensions/features.yml +7 -1
  60. cmdbox/extensions/sample_project/sample/app/features/cli/sample_client_time.py +2 -2
  61. cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +3 -3
  62. cmdbox/licenses/LICENSE_APScheduler_3_11_0_MIT_License.txt +19 -0
  63. cmdbox/licenses/LICENSE_SQLAlchemy_2_0_43_MIT.txt +19 -0
  64. cmdbox/licenses/LICENSE_Werkzeug_3_1_1_BSD_License.txt +28 -0
  65. cmdbox/licenses/LICENSE_absolufy-imports_0_3_1_MIT_License.txt +21 -0
  66. cmdbox/licenses/LICENSE_anyio_4_10_0_UNKNOWN.txt +20 -0
  67. cmdbox/licenses/{LICENSE_typer_0_16_0_MIT_License.txt → LICENSE_backoff_2_2_1_MIT_License.txt} +1 -1
  68. cmdbox/licenses/LICENSE_certifi_2025_8_3_Mozilla_Public_License_2_0-MPL_2_0.txt +20 -0
  69. cmdbox/licenses/LICENSE_charset-normalizer_3_4_3_MIT.txt +21 -0
  70. cmdbox/licenses/LICENSE_cryptography_45_0_6_Apache-2_0_OR_BSD-3-Clause.txt +3 -0
  71. cmdbox/licenses/LICENSE_cyclopts_3_22_5_Apache_Software_License.txt +201 -0
  72. cmdbox/licenses/LICENSE_fastapi-sso_0_18_0_MIT_License.txt +21 -0
  73. cmdbox/licenses/LICENSE_fastmcp_2_11_3_Apache_Software_License.txt +201 -0
  74. cmdbox/licenses/LICENSE_google-adk_1_9_0_Apache_Software_License.txt +202 -0
  75. cmdbox/licenses/LICENSE_google-genai_1_28_0_Apache_Software_License.txt +202 -0
  76. cmdbox/licenses/LICENSE_google-genai_1_29_0_Apache_Software_License.txt +202 -0
  77. cmdbox/licenses/LICENSE_greenlet_3_2_4_MIT_AND_Python-2_0.txt +30 -0
  78. cmdbox/licenses/LICENSE_isodate_0_7_2_BSD_License.txt +26 -0
  79. cmdbox/licenses/LICENSE_lazy-object-proxy_1_11_0_BSD_License.txt +20 -0
  80. cmdbox/licenses/LICENSE_litellm-enterprise_0_1_19_UNKNOWN.txt +37 -0
  81. cmdbox/licenses/LICENSE_litellm_1_75_5_post1_MIT_License.txt +26 -0
  82. cmdbox/licenses/LICENSE_markdown-it-py_4_0_0_MIT_License.txt +21 -0
  83. cmdbox/licenses/LICENSE_mcp_1_12_4_MIT_License.txt +21 -0
  84. cmdbox/licenses/LICENSE_multidict_6_6_4_Apache_License_2_0.txt +13 -0
  85. cmdbox/licenses/LICENSE_oauthlib_3_3_1_BSD-3-Clause.txt +27 -0
  86. cmdbox/licenses/LICENSE_openai_1_99_9_Apache_Software_License.txt +201 -0
  87. cmdbox/licenses/LICENSE_openapi-core_0_19_5_BSD_License.txt +29 -0
  88. cmdbox/licenses/LICENSE_openapi-schema-validator_0_6_3_BSD_License.txt +29 -0
  89. cmdbox/licenses/LICENSE_openapi-spec-validator_0_7_2_Apache_Software_License.txt +201 -0
  90. cmdbox/licenses/LICENSE_opentelemetry-api_1_36_0_UNKNOWN.txt +201 -0
  91. cmdbox/licenses/LICENSE_opentelemetry-sdk_1_36_0_UNKNOWN.txt +201 -0
  92. cmdbox/licenses/LICENSE_opentelemetry-semantic-conventions_0_57b0_UNKNOWN.txt +201 -0
  93. cmdbox/licenses/LICENSE_orjson_3_11_1_Apache_Software_License-MIT_License.txt +201 -0
  94. cmdbox/licenses/LICENSE_parse_1_20_2_MIT_License.txt +19 -0
  95. cmdbox/licenses/LICENSE_pathable_0_4_4_Apache_Software_License.txt +201 -0
  96. cmdbox/licenses/{LICENSE_pillow_11_2_1_UNKNOWN.txt → LICENSE_pillow_11_3_0_UNKNOWN.txt} +393 -3
  97. cmdbox/licenses/LICENSE_pyperclip_1_9_0_BSD_License.txt +27 -0
  98. cmdbox/licenses/LICENSE_redis_6_4_0_MIT_License.txt +21 -0
  99. cmdbox/licenses/LICENSE_rfc3339-validator_0_1_4_MIT_License.txt +22 -0
  100. cmdbox/licenses/LICENSE_rich-rst_1_3_1_MIT_License.txt +7 -0
  101. cmdbox/licenses/LICENSE_rpds-py_0_27_0_UNKNOWN.txt +19 -0
  102. cmdbox/licenses/{LICENSE_setuptools_65_5_0_MIT_License.txt → LICENSE_setuptools_80_9_0_UNKNOWN.txt} +0 -2
  103. cmdbox/licenses/LICENSE_sphinx-intl_2_3_2_UNKNOWN.txt +25 -0
  104. cmdbox/licenses/LICENSE_tenacity_9_1_2_Apache_Software_License.txt +202 -0
  105. cmdbox/licenses/LICENSE_tiktoken_0_11_0_MIT_License-Copyright-c-2022_OpenAI-Shantanu_Jain-Permission_is_hereby_granted-free_of_charge-to_any_per.txt +21 -0
  106. cmdbox/licenses/LICENSE_tokenizers_0_21_4_Apache_Software_License.txt +1 -0
  107. cmdbox/licenses/LICENSE_voicevox_core_0_16_0_MIT.txt +20 -0
  108. cmdbox/licenses/LICENSE_watchdog_6_0_0_Apache_Software_License.txt +16 -0
  109. cmdbox/licenses/{LICENSE_httptools_0_6_4_MIT_License.txt → LICENSE_wsproto_1_2_0_MIT_License.txt} +3 -3
  110. cmdbox/licenses/files.txt +69 -50
  111. cmdbox/logconf_cmdbox.yml +136 -0
  112. cmdbox/version.py +2 -2
  113. cmdbox/web/agent.html +16 -2
  114. cmdbox/web/assets/cmdbox/agent.js +226 -1
  115. cmdbox/web/assets/cmdbox/common.js +20 -25
  116. cmdbox/web/assets/cmdbox/svgicon.js +18 -0
  117. {cmdbox-0.6.3.2.dist-info → cmdbox-0.6.4.1.dist-info}/METADATA +29 -20
  118. {cmdbox-0.6.3.2.dist-info → cmdbox-0.6.4.1.dist-info}/RECORD +158 -118
  119. {cmdbox-0.6.3.2.dist-info → cmdbox-0.6.4.1.dist-info}/WHEEL +1 -1
  120. cmdbox/config.yml +0 -3
  121. cmdbox/licenses/LICENSE_shellingham_1_5_4_ISC_License-ISCL.txt +0 -13
  122. cmdbox/licenses/LICENSE_watchfiles_1_1_0_MIT_License.txt +0 -21
  123. cmdbox/logconf_audit.yml +0 -43
  124. cmdbox/logconf_client.yml +0 -43
  125. cmdbox/logconf_edge.yml +0 -43
  126. cmdbox/logconf_gui.yml +0 -43
  127. cmdbox/logconf_mcp.yml +0 -43
  128. cmdbox/logconf_server.yml +0 -43
  129. cmdbox/logconf_web.yml +0 -43
  130. /cmdbox/licenses/{LICENSE_Authlib_1_6_0_BSD_License.txt → LICENSE_Authlib_1_6_1_BSD_License.txt} +0 -0
  131. /cmdbox/licenses/{LICENSE_SQLAlchemy_2_0_41_MIT.txt → LICENSE_SQLAlchemy_2_0_42_MIT.txt} +0 -0
  132. /cmdbox/licenses/{LICENSE_aiohttp_3_12_13_Apache-2_0.txt → LICENSE_aiohttp_3_12_15_Apache-2_0_AND_MIT.txt} +0 -0
  133. /cmdbox/licenses/{LICENSE_aiosignal_1_3_2_Apache_Software_License.txt → LICENSE_aiosignal_1_4_0_Apache_Software_License.txt} +0 -0
  134. /cmdbox/licenses/{LICENSE_certifi_2025_6_15_Mozilla_Public_License_2_0-MPL_2_0.txt → LICENSE_certifi_2025_7_14_Mozilla_Public_License_2_0-MPL_2_0.txt} +0 -0
  135. /cmdbox/licenses/{LICENSE_cryptography_45_0_4_Apache-2_0_OR_BSD-3-Clause.txt → LICENSE_cryptography_45_0_5_Apache-2_0_OR_BSD-3-Clause.txt} +0 -0
  136. /cmdbox/licenses/{LICENSE_docstring_parser_0_16_MIT_License.txt → LICENSE_docstring_parser_0_17_0_MIT_License.txt} +0 -0
  137. /cmdbox/licenses/{LICENSE_fastapi_0_115_14_MIT_License.txt → LICENSE_fastapi_0_116_1_MIT_License.txt} +0 -0
  138. /cmdbox/licenses/{LICENSE_fastmcp_2_10_1_Apache_Software_License.txt → LICENSE_fastmcp_2_11_0_Apache_Software_License.txt} +0 -0
  139. /cmdbox/licenses/{LICENSE_fsspec_2025_5_1_BSD_License.txt → LICENSE_fsspec_2025_7_0_BSD_License.txt} +0 -0
  140. /cmdbox/licenses/{LICENSE_google-adk_1_5_0_Apache_Software_License.txt → LICENSE_google-adk_1_10_0_Apache_Software_License.txt} +0 -0
  141. /cmdbox/licenses/{LICENSE_google-api-python-client_2_174_0_Apache_Software_License.txt → LICENSE_google-api-python-client_2_177_0_Apache_Software_License.txt} +0 -0
  142. /cmdbox/licenses/{LICENSE_huggingface-hub_0_33_1_Apache_Software_License.txt → LICENSE_google-api-python-client_2_178_0_Apache_Software_License.txt} +0 -0
  143. /cmdbox/licenses/{LICENSE_google-cloud-aiplatform_1_100_0_Apache_2_0.txt → LICENSE_google-cloud-aiplatform_1_106_0_Apache_2_0.txt} +0 -0
  144. /cmdbox/licenses/{LICENSE_google-cloud-bigquery_3_34_0_Apache_Software_License.txt → LICENSE_google-cloud-aiplatform_1_108_0_Apache_2_0.txt} +0 -0
  145. /cmdbox/licenses/{LICENSE_google-genai_1_23_0_Apache_Software_License.txt → LICENSE_google-cloud-bigquery_3_35_1_Apache_Software_License.txt} +0 -0
  146. /cmdbox/licenses/{LICENSE_grpcio-status_1_73_1_Apache_Software_License.txt → LICENSE_grpcio-status_1_74_0_Apache_Software_License.txt} +0 -0
  147. /cmdbox/licenses/{LICENSE_grpcio_1_73_1_Apache_Software_License.txt → LICENSE_grpcio_1_74_0_Apache_Software_License.txt} +0 -0
  148. /cmdbox/licenses/{LICENSE_opentelemetry-api_1_34_1_Apache_Software_License.txt → LICENSE_huggingface-hub_0_34_3_Apache_Software_License.txt} +0 -0
  149. /cmdbox/licenses/{LICENSE_opentelemetry-sdk_1_34_1_Apache_Software_License.txt → LICENSE_huggingface-hub_0_34_4_Apache_Software_License.txt} +0 -0
  150. /cmdbox/licenses/{LICENSE_opentelemetry-semantic-conventions_0_55b1_Apache_Software_License.txt → LICENSE_jsonschema-path_0_3_4_Apache_Software_License.txt} +0 -0
  151. /cmdbox/licenses/{LICENSE_jsonschema_4_24_0_UNKNOWN.txt → LICENSE_jsonschema_4_25_0_UNKNOWN.txt} +0 -0
  152. /cmdbox/licenses/{LICENSE_litellm_1_73_6_MIT_License.txt → LICENSE_litellm_1_74_12_MIT_License.txt} +0 -0
  153. /cmdbox/licenses/{LICENSE_mcp_1_10_1_MIT_License.txt → LICENSE_mcp_1_12_3_MIT_License.txt} +0 -0
  154. /cmdbox/licenses/{LICENSE_multidict_6_6_2_Apache_License_2_0.txt → LICENSE_multidict_6_6_3_Apache_License_2_0.txt} +0 -0
  155. /cmdbox/licenses/{LICENSE_nh3_0_2_21_MIT.txt → LICENSE_nh3_0_3_0_MIT.txt} +0 -0
  156. /cmdbox/licenses/{LICENSE_numpy_2_3_1_BSD_License.txt → LICENSE_numpy_2_3_2_BSD_License.txt} +0 -0
  157. /cmdbox/licenses/{LICENSE_openai_1_93_0_Apache_Software_License.txt → LICENSE_openai_1_98_0_Apache_Software_License.txt} +0 -0
  158. /cmdbox/licenses/{LICENSE_tokenizers_0_21_2_Apache_Software_License.txt → LICENSE_pywin32_311_Python_Software_Foundation_License.txt} +0 -0
  159. /cmdbox/licenses/{LICENSE_regex_2024_11_6_Apache_Software_License.txt → LICENSE_regex_2025_7_34_UNKNOWN.txt} +0 -0
  160. /cmdbox/licenses/{LICENSE_rich_14_0_0_MIT_License.txt → LICENSE_rich_14_1_0_MIT_License.txt} +0 -0
  161. /cmdbox/licenses/{LICENSE_rpds-py_0_25_1_MIT.txt → LICENSE_rpds-py_0_26_0_MIT.txt} +0 -0
  162. /cmdbox/licenses/{LICENSE_sse-starlette_2_3_6_BSD_License.txt → LICENSE_sse-starlette_3_0_2_UNKNOWN.txt} +0 -0
  163. /cmdbox/licenses/{LICENSE_starlette_0_46_2_BSD_License.txt → LICENSE_starlette_0_47_2_BSD_License.txt} +0 -0
  164. /cmdbox/licenses/{LICENSE_typing_extensions_4_14_0_UNKNOWN.txt → LICENSE_typing_extensions_4_14_1_UNKNOWN.txt} +0 -0
  165. /cmdbox/licenses/{LICENSE_zope_event_5_1_Zope_Public_License.txt → LICENSE_zope_event_5_1_1_Zope_Public_License.txt} +0 -0
  166. {cmdbox-0.6.3.2.dist-info → cmdbox-0.6.4.1.dist-info}/entry_points.txt +0 -0
  167. {cmdbox-0.6.3.2.dist-info → cmdbox-0.6.4.1.dist-info/licenses}/LICENSE +0 -0
  168. {cmdbox-0.6.3.2.dist-info → cmdbox-0.6.4.1.dist-info}/top_level.txt +0 -0
cmdbox/app/server.py CHANGED
@@ -1,224 +1,224 @@
1
- from pathlib import Path
2
- from cmdbox.app import common, filer, feature, options
3
- from cmdbox.app.commons import redis_client
4
- from redis import exceptions
5
- from typing import List, Dict, Any
6
- import logging
7
- import redis
8
- import time
9
-
10
-
11
- class Server(filer.Filer):
12
-
13
- def __init__(self, data_dir:Path, logger:logging.Logger, redis_host:str="localhost", redis_port:int=6379, redis_password:str=None, svname:str='server'):
14
- """
15
- Redisサーバーに接続し、クライアントからのコマンドを受信し実行する
16
-
17
- Args:
18
- data_dir (Path): データフォルダのパス
19
- logger (logging): ロガー
20
- redis_host (str): Redisホスト名, by default "localhost"
21
- redis_port (int): Redisポート番号, by default 6379
22
- redis_password (str): Redisパスワード, by default None
23
- svname (str, optional): サーバーのサービス名. by default 'server'
24
- """
25
- super().__init__(data_dir, logger)
26
- if svname.find('-') >= 0:
27
- raise ValueError(f"Server name is invalid. '-' is not allowed. svname={svname}")
28
- self.redis_host = redis_host
29
- self.redis_port = redis_port
30
- self.redis_password = redis_password
31
- self.org_svname = svname
32
- self.svname = f"{svname}-{common.random_string(size=6)}"
33
- self.redis_cli = None
34
- self.sessions:Dict[str, Dict[str, Any]] = {}
35
- self.is_running = False
36
- self.train_thread = None
37
- self.cleaning_interval = 60
38
- if self.logger.level == logging.DEBUG:
39
- self.logger.debug(f"server init parameter: data={self.data_dir} -> {self.data_dir.absolute()}")
40
- self.logger.debug(f"server init parameter: redis_host={self.redis_host}")
41
- self.logger.debug(f"server init parameter: redis_port={self.redis_port}")
42
- self.logger.debug(f"server init parameter: redis_password=********")
43
- self.logger.debug(f"server init parameter: svname={self.svname}")
44
- self.options = options.Options.getInstance()
45
-
46
- def __enter__(self):
47
- self.start_server()
48
- return self
49
-
50
- def __exit__(self, a, b, c):
51
- self.terminate_server()
52
-
53
- def start_server(self, retry_count:int=20, retry_interval:int=5):
54
- """
55
- サーバー処理を開始する
56
- """
57
- self.is_running = False
58
- self.retry_count = retry_count
59
- self.retry_interval = retry_interval
60
- if self.logger.level == logging.DEBUG:
61
- self.logger.debug(f"server start parameter: retry_count={self.retry_count}")
62
- self.logger.debug(f"server start parameter: retry_interval={self.retry_interval}")
63
- self.redis_cli = redis_client.RedisClient(self.logger, host=self.redis_host, port=self.redis_port, password=self.redis_password, svname=self.svname)
64
- if self.redis_cli.check_server(find_svname=False, retry_count=self.retry_count, retry_interval=self.retry_interval, outstatus=True):
65
- self.is_running = True
66
- self._run_server()
67
-
68
- def list_server(self) -> Dict[str, List[Dict[str, Any]]]:
69
- """
70
- 起動しているサーバーリストを取得する
71
-
72
- Returns:
73
- Dict[str, List[Dict[str, Any]]]: サーバーのリスト
74
- """
75
- if self.redis_cli is None:
76
- self.redis_cli = redis_client.RedisClient(self.logger, host=self.redis_host, port=self.redis_port, password=self.redis_password, svname=self.svname)
77
- svlist = self.redis_cli.list_server()
78
- if len(svlist) <= 0:
79
- return dict(warn="No server is running.")
80
- return dict(success=svlist)
81
-
82
- def _clean_server(self):
83
- """
84
- Redisサーバーに残っている停止済みのサーバーキーを削除する
85
- """
86
- hblist = self.redis_cli.keys("hb-*")
87
- for hb in hblist:
88
- hb = hb.decode()
89
- try:
90
- v = self.redis_cli.hget(hb, 'ctime')
91
- if v is None:
92
- continue
93
- except exceptions.ResponseError:
94
- self.logger.warning(f"Failed to get ctime. {hb}", exc_info=True)
95
- continue
96
- tm = time.time() - float(v)
97
- if tm > self.cleaning_interval:
98
- self.redis_cli.delete(hb)
99
- self.redis_cli.delete(hb.replace("hb-", "sv-"))
100
-
101
- def _clean_reskey(self):
102
- """
103
- Redisサーバーに残っている停止済みのクライアントキーを削除する
104
- """
105
- rlist = self.redis_cli.keys("cl-*")
106
- for reskey in rlist:
107
- try:
108
- tm = int(reskey.decode().split("-")[2])
109
- if time.time() - tm > self.cleaning_interval:
110
- self.redis_cli.delete(reskey)
111
- except Exception as e:
112
- self.redis_cli.delete(reskey)
113
-
114
- def _run_server(self):
115
- self.logger.info(f"start server. svname={self.svname}")
116
- ltime = time.time()
117
- receive_cnt = 0
118
- sccess_cnt = 0
119
- warn_cnt = 0
120
- error_cnt = 0
121
- self.redis_cli.hset(self.redis_cli.hbname, 'receive_cnt', receive_cnt)
122
- self.redis_cli.hset(self.redis_cli.hbname, 'sccess_cnt', sccess_cnt)
123
- self.redis_cli.hset(self.redis_cli.hbname, 'warn_cnt', warn_cnt)
124
- self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
125
-
126
- def _publish(msg_str):
127
- # 各サーバーにメッセージを配布する
128
- hblist = self.redis_cli.keys(f"hb-{self.org_svname}-*")
129
- for hb in hblist:
130
- hb = hb.decode()
131
- sv = hb.replace("hb-", "sv-")
132
- self.redis_cli.rpush(sv, msg_str)
133
-
134
- while self.is_running:
135
- try:
136
- msg = None
137
- # ブロッキングリストから要素を取り出す
138
- ctime = time.time()
139
- self.redis_cli.hset(self.redis_cli.hbname, 'ctime', ctime)
140
- self.redis_cli.hset(self.redis_cli.hbname, 'status', 'ready')
141
- result = self.redis_cli.blpop(self.redis_cli.svname)
142
- if ctime - ltime > self.cleaning_interval:
143
- self._clean_server()
144
- self._clean_reskey()
145
- ltime = ctime
146
- to_cluster = False
147
- if result is None or len(result) <= 0:
148
- # クラスター宛メッセージがあるか確認する
149
- result = self.redis_cli.blpop(f"sv-{self.org_svname}")
150
- if result is None or len(result) <= 0:
151
- time.sleep(1)
152
- continue
153
- to_cluster = True
154
- msg_str = result[1].decode()
155
- msg = msg_str.split(' ')
156
- if len(msg) <= 0:
157
- time.sleep(1)
158
- continue
159
-
160
- st = None
161
- receive_cnt += 1
162
- self.redis_cli.hset(self.redis_cli.hbname, 'receive_cnt', receive_cnt)
163
- self.redis_cli.hset(self.redis_cli.hbname, 'status', 'processing')
164
-
165
- svcmd_feature:feature.Feature = self.options.get_svcmd_feature(msg[0])
166
- if svcmd_feature is not None:
167
- if to_cluster and svcmd_feature.is_cluster_redirect():
168
- _publish(msg_str)
169
- continue
170
- if msg[0] == 'stop_server':
171
- self.is_running = False
172
- st = svcmd_feature.svrun(self.data_dir, self.logger, self.redis_cli, msg, self.sessions)
173
- else:
174
- self.logger.warning(f"Unknown command {msg}")
175
- st = self.RESP_WARN
176
-
177
- if st==self.RESP_SCCESS:
178
- sccess_cnt += 1
179
- self.redis_cli.hset(self.redis_cli.hbname, 'sccess_cnt', sccess_cnt)
180
- elif st==self.RESP_WARN:
181
- warn_cnt += 1
182
- self.redis_cli.hset(self.redis_cli.hbname, 'warn_cnt', warn_cnt)
183
- elif st==self.RESP_ERROR:
184
- error_cnt += 1
185
- self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
186
- self.redis_cli.hset(self.redis_cli.hbname, 'ctime', time.time())
187
- except exceptions.TimeoutError:
188
- pass
189
- except exceptions.ConnectionError as e:
190
- self.logger.warning(f"Connection to the server was lost. {e}", exc_info=True)
191
- if not self.redis_cli.check_server(find_svname=False, retry_count=self.retry_count, retry_interval=self.retry_interval, outstatus=True):
192
- self.is_running = False
193
- break
194
- except OSError as e:
195
- self.logger.warning(f"OSError. {e}. This message is not executable in the server environment. ({msg})", exc_info=True)
196
- if msg is not None and len(msg) > 1:
197
- self.redis_cli.rpush(msg[1], dict(warn=f"OSError. {e}. This message is not executable in the server environment. ({msg[0]})"))
198
- error_cnt += 1
199
- self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
200
- pass
201
- except IndexError as e:
202
- self.logger.warning(f"IndexError. {e}. The message received by the server is invalid. ({msg})", exc_info=True)
203
- if msg is not None and len(msg) > 1:
204
- self.redis_cli.rpush(msg[1], dict(warn=f"IndexError. {e}. The message received by the server is invalid. ({msg[0]})"))
205
- error_cnt += 1
206
- self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
207
- pass
208
- except KeyboardInterrupt as e:
209
- self.is_running = False
210
- break
211
- except Exception as e:
212
- self.logger.warning(f"Unknown error occurred. {e}. Service will be stopped due to unknown cause.({msg})", exc_info=True)
213
- self.is_running = False
214
- break
215
- self.redis_cli.delete(self.redis_cli.svname)
216
- self.redis_cli.delete(self.redis_cli.hbname)
217
- self.logger.info(f"stop server. svname={self.redis_cli.svname}")
218
-
219
- def terminate_server(self):
220
- """
221
- サーバー処理を終了する
222
- """
223
- self.redis_cli.close()
224
- self.logger.info(f"terminate server.")
1
+ from pathlib import Path
2
+ from cmdbox.app import common, filer, feature, options
3
+ from cmdbox.app.commons import redis_client
4
+ from redis import exceptions
5
+ from typing import List, Dict, Any
6
+ import logging
7
+ import redis
8
+ import time
9
+
10
+
11
+ class Server(filer.Filer):
12
+
13
+ def __init__(self, data_dir:Path, logger:logging.Logger, redis_host:str="localhost", redis_port:int=6379, redis_password:str=None, svname:str='server'):
14
+ """
15
+ Redisサーバーに接続し、クライアントからのコマンドを受信し実行する
16
+
17
+ Args:
18
+ data_dir (Path): データフォルダのパス
19
+ logger (logging): ロガー
20
+ redis_host (str): Redisホスト名, by default "localhost"
21
+ redis_port (int): Redisポート番号, by default 6379
22
+ redis_password (str): Redisパスワード, by default None
23
+ svname (str, optional): サーバーのサービス名. by default 'server'
24
+ """
25
+ super().__init__(data_dir, logger)
26
+ if svname.find('-') >= 0:
27
+ raise ValueError(f"Server name is invalid. '-' is not allowed. svname={svname}")
28
+ self.redis_host = redis_host
29
+ self.redis_port = redis_port
30
+ self.redis_password = redis_password
31
+ self.org_svname = svname
32
+ self.svname = f"{svname}-{common.random_string(size=6)}"
33
+ self.redis_cli = None
34
+ self.sessions:Dict[str, Dict[str, Any]] = {}
35
+ self.is_running = False
36
+ self.train_thread = None
37
+ self.cleaning_interval = 60
38
+ if self.logger.level == logging.DEBUG:
39
+ self.logger.debug(f"server init parameter: data={self.data_dir} -> {self.data_dir.absolute()}")
40
+ self.logger.debug(f"server init parameter: redis_host={self.redis_host}")
41
+ self.logger.debug(f"server init parameter: redis_port={self.redis_port}")
42
+ self.logger.debug(f"server init parameter: redis_password=********")
43
+ self.logger.debug(f"server init parameter: svname={self.svname}")
44
+ self.options = options.Options.getInstance()
45
+
46
+ def __enter__(self):
47
+ self.start_server()
48
+ return self
49
+
50
+ def __exit__(self, a, b, c):
51
+ self.terminate_server()
52
+
53
+ def start_server(self, retry_count:int=20, retry_interval:int=5):
54
+ """
55
+ サーバー処理を開始する
56
+ """
57
+ self.is_running = False
58
+ self.retry_count = retry_count
59
+ self.retry_interval = retry_interval
60
+ if self.logger.level == logging.DEBUG:
61
+ self.logger.debug(f"server start parameter: retry_count={self.retry_count}")
62
+ self.logger.debug(f"server start parameter: retry_interval={self.retry_interval}")
63
+ self.redis_cli = redis_client.RedisClient(self.logger, host=self.redis_host, port=self.redis_port, password=self.redis_password, svname=self.svname)
64
+ if self.redis_cli.check_server(find_svname=False, retry_count=self.retry_count, retry_interval=self.retry_interval, outstatus=True):
65
+ self.is_running = True
66
+ self._run_server()
67
+
68
+ def list_server(self) -> Dict[str, List[Dict[str, Any]]]:
69
+ """
70
+ 起動しているサーバーリストを取得する
71
+
72
+ Returns:
73
+ Dict[str, List[Dict[str, Any]]]: サーバーのリスト
74
+ """
75
+ if self.redis_cli is None:
76
+ self.redis_cli = redis_client.RedisClient(self.logger, host=self.redis_host, port=self.redis_port, password=self.redis_password, svname=self.svname)
77
+ svlist = self.redis_cli.list_server()
78
+ if len(svlist) <= 0:
79
+ return dict(warn="No server is running.")
80
+ return dict(success=svlist)
81
+
82
+ def _clean_server(self):
83
+ """
84
+ Redisサーバーに残っている停止済みのサーバーキーを削除する
85
+ """
86
+ hblist = self.redis_cli.keys("hb-*")
87
+ for hb in hblist:
88
+ hb = hb.decode()
89
+ try:
90
+ v = self.redis_cli.hget(hb, 'ctime')
91
+ if v is None:
92
+ continue
93
+ except exceptions.ResponseError:
94
+ self.logger.warning(f"Failed to get ctime. {hb}", exc_info=True)
95
+ continue
96
+ tm = time.time() - float(v)
97
+ if tm > self.cleaning_interval:
98
+ self.redis_cli.delete(hb)
99
+ self.redis_cli.delete(hb.replace("hb-", "sv-"))
100
+
101
+ def _clean_reskey(self):
102
+ """
103
+ Redisサーバーに残っている停止済みのクライアントキーを削除する
104
+ """
105
+ rlist = self.redis_cli.keys("cl-*")
106
+ for reskey in rlist:
107
+ try:
108
+ tm = int(reskey.decode().split("-")[2])
109
+ if time.time() - tm > self.cleaning_interval:
110
+ self.redis_cli.delete(reskey)
111
+ except Exception as e:
112
+ self.redis_cli.delete(reskey)
113
+
114
+ def _run_server(self):
115
+ self.logger.info(f"start server. svname={self.svname}")
116
+ ltime = time.time()
117
+ receive_cnt = 0
118
+ sccess_cnt = 0
119
+ warn_cnt = 0
120
+ error_cnt = 0
121
+ self.redis_cli.hset(self.redis_cli.hbname, 'receive_cnt', receive_cnt)
122
+ self.redis_cli.hset(self.redis_cli.hbname, 'sccess_cnt', sccess_cnt)
123
+ self.redis_cli.hset(self.redis_cli.hbname, 'warn_cnt', warn_cnt)
124
+ self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
125
+
126
+ def _publish(msg_str):
127
+ # 各サーバーにメッセージを配布する
128
+ hblist = self.redis_cli.keys(f"hb-{self.org_svname}-*")
129
+ for hb in hblist:
130
+ hb = hb.decode()
131
+ sv = hb.replace("hb-", "sv-")
132
+ self.redis_cli.rpush(sv, msg_str)
133
+
134
+ while self.is_running:
135
+ try:
136
+ msg = None
137
+ # ブロッキングリストから要素を取り出す
138
+ ctime = time.time()
139
+ self.redis_cli.hset(self.redis_cli.hbname, 'ctime', ctime)
140
+ self.redis_cli.hset(self.redis_cli.hbname, 'status', 'ready')
141
+ result = self.redis_cli.blpop(self.redis_cli.svname)
142
+ if ctime - ltime > self.cleaning_interval:
143
+ self._clean_server()
144
+ self._clean_reskey()
145
+ ltime = ctime
146
+ to_cluster = False
147
+ if result is None or len(result) <= 0:
148
+ # クラスター宛メッセージがあるか確認する
149
+ result = self.redis_cli.blpop(f"sv-{self.org_svname}")
150
+ if result is None or len(result) <= 0:
151
+ time.sleep(1)
152
+ continue
153
+ to_cluster = True
154
+ msg_str = result[1].decode()
155
+ msg = msg_str.split(' ')
156
+ if len(msg) <= 0:
157
+ time.sleep(1)
158
+ continue
159
+
160
+ st = None
161
+ receive_cnt += 1
162
+ self.redis_cli.hset(self.redis_cli.hbname, 'receive_cnt', receive_cnt)
163
+ self.redis_cli.hset(self.redis_cli.hbname, 'status', 'processing')
164
+
165
+ svcmd_feature:feature.Feature = self.options.get_svcmd_feature(msg[0])
166
+ if svcmd_feature is not None:
167
+ if to_cluster and svcmd_feature.is_cluster_redirect():
168
+ _publish(msg_str)
169
+ continue
170
+ if msg[0] == 'stop_server':
171
+ self.is_running = False
172
+ st = svcmd_feature.svrun(self.data_dir, self.logger, self.redis_cli, msg, self.sessions)
173
+ else:
174
+ self.logger.warning(f"Unknown command {msg}")
175
+ st = self.RESP_WARN
176
+
177
+ if st==self.RESP_SUCCESS:
178
+ sccess_cnt += 1
179
+ self.redis_cli.hset(self.redis_cli.hbname, 'sccess_cnt', sccess_cnt)
180
+ elif st==self.RESP_WARN:
181
+ warn_cnt += 1
182
+ self.redis_cli.hset(self.redis_cli.hbname, 'warn_cnt', warn_cnt)
183
+ elif st==self.RESP_ERROR:
184
+ error_cnt += 1
185
+ self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
186
+ self.redis_cli.hset(self.redis_cli.hbname, 'ctime', time.time())
187
+ except exceptions.TimeoutError:
188
+ pass
189
+ except exceptions.ConnectionError as e:
190
+ self.logger.warning(f"Connection to the server was lost. {e}", exc_info=True)
191
+ if not self.redis_cli.check_server(find_svname=False, retry_count=self.retry_count, retry_interval=self.retry_interval, outstatus=True):
192
+ self.is_running = False
193
+ break
194
+ except OSError as e:
195
+ self.logger.warning(f"OSError. {e}. This message is not executable in the server environment. ({msg})", exc_info=True)
196
+ if msg is not None and len(msg) > 1:
197
+ self.redis_cli.rpush(msg[1], dict(warn=f"OSError. {e}. This message is not executable in the server environment. ({msg[0]})"))
198
+ error_cnt += 1
199
+ self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
200
+ pass
201
+ except IndexError as e:
202
+ self.logger.warning(f"IndexError. {e}. The message received by the server is invalid. ({msg})", exc_info=True)
203
+ if msg is not None and len(msg) > 1:
204
+ self.redis_cli.rpush(msg[1], dict(warn=f"IndexError. {e}. The message received by the server is invalid. ({msg[0]})"))
205
+ error_cnt += 1
206
+ self.redis_cli.hset(self.redis_cli.hbname, 'error_cnt', error_cnt)
207
+ pass
208
+ except KeyboardInterrupt as e:
209
+ self.is_running = False
210
+ break
211
+ except Exception as e:
212
+ self.logger.warning(f"Unknown error occurred. {e}. Service will be stopped due to unknown cause.({msg})", exc_info=True)
213
+ self.is_running = False
214
+ break
215
+ self.redis_cli.delete(self.redis_cli.svname)
216
+ self.redis_cli.delete(self.redis_cli.hbname)
217
+ self.logger.info(f"stop server. svname={self.redis_cli.svname}")
218
+
219
+ def terminate_server(self):
220
+ """
221
+ サーバー処理を終了する
222
+ """
223
+ self.redis_cli.close()
224
+ self.logger.info(f"terminate server.")
cmdbox/app/web.py CHANGED
@@ -793,10 +793,15 @@ class Web:
793
793
  """
794
794
  if session_id is None:
795
795
  session_id = common.random_string(32)
796
- session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
797
- if session is None:
796
+ try:
797
+ session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
798
+ if session is None:
799
+ session = await session_service.create_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
800
+ return session
801
+ except NotImplementedError:
802
+ # セッションが1件もない場合はNotImplementedErrorが発生することがある
798
803
  session = await session_service.create_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
799
- return session
804
+ return session
800
805
  self.create_agent_session = create_agent_session
801
806
  async def list_agent_sessions(session_service:BaseSessionService, user_id:str, session_id:str=None) -> List[Session]:
802
807
  """
@@ -809,20 +814,37 @@ class Web:
809
814
  Returns:
810
815
  List[Session]: セッションリスト
811
816
  """
812
- if session_id is None:
813
- sessions = await session_service.list_sessions(app_name=self.ver.__appid__, user_id=user_id)
814
- ret = []
815
- for s in sessions.sessions:
816
- session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=s.id)
817
+ try:
818
+ if session_id is None:
819
+ sessions = await session_service.list_sessions(app_name=self.ver.__appid__, user_id=user_id)
820
+ ret = []
821
+ for s in sessions.sessions:
822
+ try:
823
+ session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=s.id)
824
+ if session is None:
825
+ continue
826
+ ret.append(session)
827
+ except:
828
+ # セッションが取得できない場合は削除する
829
+ try:
830
+ await session_service.delete_session(app_name=self.ver.__appid__, user_id=user_id, session_id=s.id)
831
+ except:
832
+ pass
833
+ finally:
834
+ continue
835
+ return ret
836
+ else:
837
+ session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
817
838
  if session is None:
818
- continue
819
- ret.append(session)
820
- return ret
821
- else:
822
- session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
823
- if session is None:
824
- return []
825
- return [session]
839
+ return []
840
+ return [session]
841
+ except NotImplementedError:
842
+ # セッションが1件もない場合はNotImplementedErrorが発生することがある
843
+ return []
844
+ except Exception as e:
845
+ # それ以外のエラーが発生した時はログに出力して空リストを返す
846
+ self.logger.warning(f"list_agent_sessions warning: {e}", exc_info=True)
847
+ return []
826
848
  self.list_agent_sessions = list_agent_sessions
827
849
  async def delete_agent_session(session_service:BaseSessionService, user_id:str, session_id:str) -> bool:
828
850
  """
@@ -841,7 +863,7 @@ class Web:
841
863
 
842
864
  mcp_app:Starlette = None
843
865
  if self.mcp is not None:
844
- mcp_app:Starlette = self.mcp.streamable_http_app()
866
+ mcp_app:Starlette = self.mcp.http_app()
845
867
  #mcp_app:Starlette = self.mcp.http_app()
846
868
  if mcp_app is not None:
847
869
  app = FastAPI(lifespan=mcp_app.lifespan)
@@ -44,7 +44,13 @@ agentrule: # Specifies a list of rules that determi
44
44
  cmds: [list, load] # Specify the "cmd" to which the rule applies. Multiple items can be specified in a list.
45
45
  rule: allow # Specifies whether the specified command is allowed or not. Values are allow or deny.
46
46
  - mode: client
47
- cmds: [http]
47
+ cmds: [file_download, file_list, http, server_info]
48
+ rule: allow
49
+ - mode: server
50
+ cmds: [list]
51
+ rule: allow
52
+ - mode: tts
53
+ cmds: [say]
48
54
  rule: allow
49
55
  audit:
50
56
  enabled: true # Specify whether to enable the audit function.
@@ -60,8 +60,8 @@ class ClientTime(feature.Feature):
60
60
  ret = dict(success=dict(data=dt.strftime('%Y-%m-%d %H:%M:%S')))
61
61
  common.print_format(ret, args.format, tm, args.output_json, args.output_json_append, pf=pf)
62
62
  if 'success' not in ret:
63
- return 1, ret, None
64
- return 0, ret, None
63
+ return self.RESP_WARN, ret, None
64
+ return self.RESP_SUCCESS, ret, None
65
65
 
66
66
  def edgerun(self, opt, tool, logger, timeout, prevres = None):
67
67
  """
@@ -92,8 +92,8 @@ class ServerTime(feature.Feature):
92
92
  retry_count=args.retry_count, retry_interval=args.retry_interval, timeout=args.timeout)
93
93
  common.print_format(ret, args.format, tm, args.output_json, args.output_json_append, pf=pf)
94
94
  if 'success' not in ret:
95
- return 1, ret, None
96
- return 0, ret, None
95
+ return self.RESP_WARN, ret, None
96
+ return self.RESP_SUCCESS, ret, None
97
97
 
98
98
  def is_cluster_redirect(self):
99
99
  """
@@ -124,7 +124,7 @@ class ServerTime(feature.Feature):
124
124
  dt = datetime.datetime.now(tz)
125
125
  ret = dict(success=dict(data=dt.strftime('%Y-%m-%d %H:%M:%S')))
126
126
  redis_cli.rpush(msg[1], ret)
127
- return self.RESP_SCCESS
127
+ return self.RESP_SUCCESS
128
128
 
129
129
  def edgerun(self, opt, tool, logger, timeout, prevres = None):
130
130
  """
@@ -0,0 +1,19 @@
1
+ This is the MIT license: http://www.opensource.org/licenses/mit-license.php
2
+
3
+ Copyright (c) Alex Grönholm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
6
+ software and associated documentation files (the "Software"), to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy, modify, merge,
8
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
9
+ to whom the Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or
12
+ substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Copyright 2005-2025 SQLAlchemy authors and contributors <see AUTHORS file>.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ Copyright 2007 Pallets
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.