dida-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dida/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """事咚咚 - 个人待办管理 CLI"""
2
+
3
+ __version__ = "0.1.0"
dida/cli.py ADDED
@@ -0,0 +1,302 @@
1
+ """CLI 主入口
2
+
3
+ 使用 Typer 构建命令行接口,使用 dong-core 的 json_output 装饰器。
4
+ """
5
+
6
+ import typer
7
+ from datetime import datetime
8
+ from dong import json_output, ValidationError, NotFoundError
9
+
10
+ from . import __version__
11
+ from .db.connection import init_db, get_connection
12
+ from .const import DB_PATH, PRIORITIES
13
+
14
+ app = typer.Typer(
15
+ name="dida",
16
+ help=f"事咚咚 - 个人待办管理 CLI (v{__version__})",
17
+ no_args_is_help=True,
18
+ add_completion=False,
19
+ )
20
+
21
+
22
+ @app.callback()
23
+ def main_callback(
24
+ version: bool = typer.Option(False, "--version", "-v", help="显示版本"),
25
+ ) -> None:
26
+ """主回调"""
27
+ if version:
28
+ typer.echo(f"dida {__version__}")
29
+ raise typer.Exit()
30
+
31
+
32
+ @app.command()
33
+ @json_output
34
+ def init():
35
+ """初始化数据库"""
36
+ init_db()
37
+ return {
38
+ "message": "数据库初始化成功",
39
+ "db_path": str(DB_PATH)
40
+ }
41
+
42
+
43
+ @app.command()
44
+ @json_output
45
+ def add(
46
+ content: str = typer.Argument(..., help="待办内容"),
47
+ due: str = typer.Option(None, "--due", "-d", help="截止时间 (YYYY-MM-DD HH:MM)"),
48
+ priority: str = typer.Option("medium", "--priority", "-p",
49
+ help=f"优先级: {','.join(PRIORITIES)}"),
50
+ note: str = typer.Option(None, "--note", "-n", help="备注"),
51
+ ):
52
+ """创建待办"""
53
+ if not content or not content.strip():
54
+ raise ValidationError("content", "待办内容不能为空")
55
+
56
+ if priority not in PRIORITIES:
57
+ raise ValidationError("priority", f"无效的优先级: {priority}")
58
+
59
+ with get_connection() as conn:
60
+ cursor = conn.cursor()
61
+ now = datetime.now().isoformat()
62
+ cursor.execute(
63
+ """
64
+ INSERT INTO todos (content, due_date, priority, note, created_at, updated_at)
65
+ VALUES (?, ?, ?, ?, ?, ?)
66
+ """,
67
+ (content.strip(), due, priority, note, now, now)
68
+ )
69
+ todo_id = cursor.lastrowid
70
+
71
+ return {"id": todo_id, "content": content, "priority": priority}
72
+
73
+
74
+ @app.command()
75
+ @json_output
76
+ def ls(
77
+ limit: int = typer.Option(20, "--limit", "-l", help="显示数量"),
78
+ completed: bool = typer.Option(None, "--completed", help="按完成状态筛选"),
79
+ priority: str = typer.Option(None, "--priority", "-p", help="按优先级筛选"),
80
+ ):
81
+ """列出待办"""
82
+ with get_connection() as conn:
83
+ cursor = conn.cursor()
84
+
85
+ conditions = []
86
+ params = []
87
+
88
+ if completed is not None:
89
+ conditions.append("completed = ?")
90
+ params.append(1 if completed else 0)
91
+
92
+ if priority:
93
+ conditions.append("priority = ?")
94
+ params.append(priority)
95
+
96
+ where_clause = " AND ".join(conditions) if conditions else "1=1"
97
+ params.append(limit)
98
+
99
+ cursor.execute(
100
+ f"""
101
+ SELECT id, content, completed, priority, due_date, created_at, note
102
+ FROM todos
103
+ WHERE {where_clause}
104
+ ORDER BY completed ASC, created_at DESC
105
+ LIMIT ?
106
+ """,
107
+ params
108
+ )
109
+ rows = cursor.fetchall()
110
+
111
+ todos = [dict(row) for row in rows]
112
+ return {"todos": todos, "total": len(todos)}
113
+
114
+
115
+ @app.command()
116
+ @json_output
117
+ def get(
118
+ todo_id: int = typer.Argument(..., help="待办 ID"),
119
+ ):
120
+ """获取待办详情"""
121
+ with get_connection() as conn:
122
+ cursor = conn.cursor()
123
+ cursor.execute(
124
+ "SELECT * FROM todos WHERE id = ?",
125
+ (todo_id,)
126
+ )
127
+ row = cursor.fetchone()
128
+
129
+ if not row:
130
+ raise NotFoundError("Todo", todo_id, message=f"未找到 ID 为 {todo_id} 的待办")
131
+
132
+ return dict(row)
133
+
134
+
135
+ @app.command()
136
+ @json_output
137
+ def done(
138
+ todo_id: int = typer.Argument(..., help="待办 ID"),
139
+ ):
140
+ """标记完成"""
141
+ with get_connection() as conn:
142
+ cursor = conn.cursor()
143
+ cursor.execute("SELECT id FROM todos WHERE id = ?", (todo_id,))
144
+ if not cursor.fetchone():
145
+ raise NotFoundError("Todo", todo_id)
146
+
147
+ now = datetime.now().isoformat()
148
+ cursor.execute(
149
+ "UPDATE todos SET completed = 1, updated_at = ? WHERE id = ?",
150
+ (now, todo_id)
151
+ )
152
+
153
+ return {"id": todo_id, "completed": True}
154
+
155
+
156
+ @app.command()
157
+ @json_output
158
+ def undo(
159
+ todo_id: int = typer.Argument(..., help="待办 ID"),
160
+ ):
161
+ """取消完成"""
162
+ with get_connection() as conn:
163
+ cursor = conn.cursor()
164
+ cursor.execute("SELECT id FROM todos WHERE id = ?", (todo_id,))
165
+ if not cursor.fetchone():
166
+ raise NotFoundError("Todo", todo_id)
167
+
168
+ now = datetime.now().isoformat()
169
+ cursor.execute(
170
+ "UPDATE todos SET completed = 0, updated_at = ? WHERE id = ?",
171
+ (now, todo_id)
172
+ )
173
+
174
+ return {"id": todo_id, "completed": False}
175
+
176
+
177
+ @app.command()
178
+ @json_output
179
+ def update(
180
+ todo_id: int = typer.Argument(..., help="待办 ID"),
181
+ content: str = typer.Option(None, "--content", "-c", help="更新内容"),
182
+ due: str = typer.Option(None, "--due", "-d", help="更新截止时间"),
183
+ priority: str = typer.Option(None, "--priority", "-p", help="更新优先级"),
184
+ note: str = typer.Option(None, "--note", "-n", help="更新备注"),
185
+ ):
186
+ """更新待办"""
187
+ with get_connection() as conn:
188
+ cursor = conn.cursor()
189
+ cursor.execute("SELECT id FROM todos WHERE id = ?", (todo_id,))
190
+ if not cursor.fetchone():
191
+ raise NotFoundError("Todo", todo_id)
192
+
193
+ updates = []
194
+ params = []
195
+
196
+ if content is not None:
197
+ updates.append("content = ?")
198
+ params.append(content.strip())
199
+ if due is not None:
200
+ updates.append("due_date = ?")
201
+ params.append(due)
202
+ if priority is not None:
203
+ if priority not in PRIORITIES:
204
+ raise ValidationError("priority", f"无效的优先级: {priority}")
205
+ updates.append("priority = ?")
206
+ params.append(priority)
207
+ if note is not None:
208
+ updates.append("note = ?")
209
+ params.append(note)
210
+
211
+ if updates:
212
+ updates.append("updated_at = ?")
213
+ params.append(datetime.now().isoformat())
214
+ params.append(todo_id)
215
+
216
+ query = f"UPDATE todos SET {', '.join(updates)} WHERE id = ?"
217
+ cursor.execute(query, params)
218
+
219
+ return {"id": todo_id, "updated": True}
220
+
221
+
222
+ @app.command()
223
+ @json_output
224
+ def delete(
225
+ todo_id: int = typer.Argument(..., help="待办 ID"),
226
+ force: bool = typer.Option(False, "--force", "-f", help="强制删除"),
227
+ ):
228
+ """删除待办"""
229
+ with get_connection() as conn:
230
+ cursor = conn.cursor()
231
+ cursor.execute("SELECT id, content FROM todos WHERE id = ?", (todo_id,))
232
+ row = cursor.fetchone()
233
+
234
+ if not row:
235
+ raise NotFoundError("Todo", todo_id)
236
+
237
+ if not force:
238
+ confirm = typer.confirm(f"确定要删除待办吗?\n{row['content']}")
239
+ if not confirm:
240
+ return {"cancelled": True, "message": "已取消删除"}
241
+
242
+ cursor.execute("DELETE FROM todos WHERE id = ?", (todo_id,))
243
+
244
+ return {"deleted": True, "id": todo_id}
245
+
246
+
247
+ @app.command()
248
+ @json_output
249
+ def search(
250
+ keyword: str = typer.Argument(..., help="搜索关键词"),
251
+ limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
252
+ ):
253
+ """搜索待办"""
254
+ with get_connection() as conn:
255
+ cursor = conn.cursor()
256
+ cursor.execute(
257
+ """
258
+ SELECT * FROM todos
259
+ WHERE content LIKE ? OR note LIKE ?
260
+ ORDER BY created_at DESC
261
+ LIMIT ?
262
+ """,
263
+ (f"%{keyword}%", f"%{keyword}%", limit)
264
+ )
265
+ rows = cursor.fetchall()
266
+
267
+ todos = [dict(row) for row in rows]
268
+ return {"todos": todos, "total": len(todos), "keyword": keyword}
269
+
270
+
271
+ @app.command()
272
+ @json_output
273
+ def stats():
274
+ """统计信息"""
275
+ with get_connection() as conn:
276
+ cursor = conn.cursor()
277
+
278
+ # 总数
279
+ cursor.execute("SELECT COUNT(*) FROM todos")
280
+ total = cursor.fetchone()[0]
281
+
282
+ # 已完成
283
+ cursor.execute("SELECT COUNT(*) FROM todos WHERE completed = 1")
284
+ completed = cursor.fetchone()[0]
285
+
286
+ # 未完成
287
+ pending = total - completed
288
+
289
+ # 按优先级统计
290
+ cursor.execute("SELECT priority, COUNT(*) FROM todos GROUP BY priority")
291
+ by_priority = dict(cursor.fetchall())
292
+
293
+ return {
294
+ "total": total,
295
+ "completed": completed,
296
+ "pending": pending,
297
+ "by_priority": by_priority
298
+ }
299
+
300
+
301
+ if __name__ == "__main__":
302
+ app()
@@ -0,0 +1 @@
1
+ """命令模块"""
dida/config.py ADDED
@@ -0,0 +1,40 @@
1
+ """配置管理模块
2
+
3
+ 继承 dong.config.Config,管理 dida-cli 的用户配置。
4
+ """
5
+
6
+ from dong.config import Config
7
+
8
+
9
+ class DidaConfig(Config):
10
+ """待咚咚配置类"""
11
+
12
+ @classmethod
13
+ def get_name(cls) -> str:
14
+ return "dida"
15
+
16
+ @classmethod
17
+ def get_defaults(cls) -> dict:
18
+ return {
19
+ "default_status": "pending",
20
+ "default_priority": 0,
21
+ "default_limit": 20,
22
+ "statuses": ["pending", "in_progress", "completed", "cancelled"],
23
+ }
24
+
25
+
26
+ # 便捷函数
27
+ def get_config() -> dict:
28
+ return DidaConfig.load()
29
+
30
+ def get_default_status() -> str:
31
+ return DidaConfig.get("default_status", "pending")
32
+
33
+ def get_default_priority() -> int:
34
+ return DidaConfig.get("default_priority", 0)
35
+
36
+ def get_default_limit() -> int:
37
+ return DidaConfig.get("default_limit", 20)
38
+
39
+ def get_statuses() -> list:
40
+ return DidaConfig.get("statuses", ["pending", "in_progress", "completed", "cancelled"])
dida/const.py ADDED
@@ -0,0 +1,13 @@
1
+ """常量定义"""
2
+
3
+ from pathlib import Path
4
+
5
+ DB_DIR = Path.home() / ".dida"
6
+ DB_PATH = DB_DIR / "dida.db"
7
+
8
+ VERSION = "0.1.0"
9
+ APP_NAME = "事咚咚"
10
+
11
+ DEFAULT_LIMIT = 20
12
+
13
+ PRIORITIES = ["low", "medium", "high", "critical"]
dida/db/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """数据库层"""
2
+
3
+ from .connection import DidaDatabase, get_connection, close_connection, get_cursor, get_db_path
4
+ from .schema import DidaSchemaManager, SCHEMA_VERSION, get_schema_version, set_schema_version, is_initialized, init_database
5
+
6
+ __all__ = [
7
+ "DidaDatabase", "DidaSchemaManager",
8
+ "get_connection", "close_connection", "get_cursor", "get_db_path",
9
+ "SCHEMA_VERSION", "get_schema_version", "set_schema_version", "is_initialized", "init_database",
10
+ ]
dida/db/connection.py ADDED
@@ -0,0 +1,37 @@
1
+ """数据库连接管理模块
2
+
3
+ 继承 dong.db.Database,提供 dida-cli 专用数据库访问。
4
+ """
5
+
6
+ import sqlite3
7
+ from typing import Iterator
8
+ from contextlib import contextmanager
9
+
10
+ from dong.db import Database as DongDatabase
11
+
12
+
13
+ class DidaDatabase(DongDatabase):
14
+ """待咚咚数据库类 - 继承自 dong.db.Database
15
+
16
+ 数据库路径: ~/.dida/dida.db
17
+ """
18
+
19
+ @classmethod
20
+ def get_name(cls) -> str:
21
+ return "dida"
22
+
23
+
24
+ # 兼容性函数
25
+ def get_connection(db_path=None):
26
+ return DidaDatabase.get_connection()
27
+
28
+ def close_connection():
29
+ DidaDatabase.close_connection()
30
+
31
+ @contextmanager
32
+ def get_cursor() -> Iterator[sqlite3.Cursor]:
33
+ with DidaDatabase.get_cursor() as cur:
34
+ yield cur
35
+
36
+ def get_db_path():
37
+ return DidaDatabase.get_db_path()
dida/db/schema.py ADDED
@@ -0,0 +1,59 @@
1
+ """数据库 Schema 定义和版本管理
2
+
3
+ 继承 dong.db.SchemaManager,管理 dida-cli 的数据库 schema。
4
+ """
5
+
6
+ from dong.db import SchemaManager
7
+ from .connection import DidaDatabase
8
+
9
+ SCHEMA_VERSION = "1.0.0"
10
+
11
+
12
+ class DidaSchemaManager(SchemaManager):
13
+ """待咚咚 Schema 管理器"""
14
+
15
+ def __init__(self):
16
+ super().__init__(
17
+ db_class=DidaDatabase,
18
+ current_version=SCHEMA_VERSION
19
+ )
20
+
21
+ def init_schema(self) -> None:
22
+ self._create_todos_table()
23
+ self._create_indexes()
24
+
25
+ def _create_todos_table(self) -> None:
26
+ with DidaDatabase.get_cursor() as cur:
27
+ cur.execute("""
28
+ CREATE TABLE IF NOT EXISTS todos (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ title TEXT NOT NULL,
31
+ description TEXT,
32
+ status TEXT DEFAULT 'pending',
33
+ priority INTEGER DEFAULT 0,
34
+ due_date TEXT,
35
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
36
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
37
+ )
38
+ """)
39
+
40
+ def _create_indexes(self) -> None:
41
+ with DidaDatabase.get_cursor() as cur:
42
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status)")
43
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_todos_due_date ON todos(due_date)")
44
+
45
+
46
+ # 兼容性函数
47
+ def get_schema_version() -> str | None:
48
+ return DidaSchemaManager().get_stored_version()
49
+
50
+ def set_schema_version(version: str) -> None:
51
+ DidaDatabase.set_meta(DidaSchemaManager.VERSION_KEY, version)
52
+
53
+ def is_initialized() -> bool:
54
+ return DidaSchemaManager().is_initialized()
55
+
56
+ def init_database() -> None:
57
+ schema = DidaSchemaManager()
58
+ if not schema.is_initialized():
59
+ schema.initialize()
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: dida-cli
3
+ Version: 0.2.0
4
+ Summary: 事咚咚 - 个人待办管理的 CLI 工具
5
+ Author: gudong
6
+ License-File: LICENSE
7
+ Keywords: cli,task-management,todo
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: dong-core>=0.3.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: typer>=0.12.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
14
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # 事咚咚 (Dida)
18
+
19
+ > 管理个人待办事项的 CLI 工具 —— 为事咚咚智能体提供底层能力
20
+
21
+ ## 产品定位
22
+
23
+ > **在你最自然的地方,用最自然的方式,管理你要做的事**
24
+
25
+ ### 我们解决什么问题
26
+
27
+ | 痛点 | 描述 |
28
+ |------|------|
29
+ | 记不住 | 说过的话、答应的事转头就忘 |
30
+ | 懒得记 | 专门的 todo app 太重,打开步骤多 |
31
+ | 易遗漏 | 任务到期了才想起,或者根本没想起 |
32
+ | 难追踪 | 想知道"今天要做什么"要到处翻 |
33
+ | 不连贯 | 聊天中说要做的事,得手动复制到 todo app |
34
+
35
+ ### 核心价值
36
+
37
+ - **零摩擦**:记录一个任务 ≤ 5 秒
38
+ - **不遗漏**:到期主动提醒
39
+ - **心有数**:随时随地问,立即知道要做什么
40
+ - **可信赖**:它记住的,就是你要做的
41
+
42
+ ### 我们不做什么
43
+
44
+ | 不做 | 原因 |
45
+ |------|------|
46
+ | 复杂项目管理 | 超出个人日常范畴,交给专业工具 |
47
+ | 多人协作 | 专注个人事务 |
48
+ | 复杂依赖关系 | 保持简单,"今天要做什么"就够了 |
49
+ | 精密时间块规划 | 番茄钟、日历视图交给专业 app |
50
+
51
+ ---
52
+
53
+ ## 安装
54
+
55
+ ```bash
56
+ pip install dida-cli
57
+ ```
58
+
59
+ ## 快速开始
60
+
61
+ ```bash
62
+ # 初始化
63
+ dida init
64
+
65
+ # 记录待办
66
+ dida add "给妈妈打电话"
67
+ dida add "明天下午三点开会" --due "2026-03-16 15:00"
68
+ dida add "周五前把报告写完" --due "2026-03-14" --priority high
69
+
70
+ # 列出待办
71
+ dida ls
72
+
73
+ # 标记完成
74
+ dida done 1
75
+
76
+ # 删除
77
+ dida delete 1
78
+ ```
79
+
80
+ ## 命令
81
+
82
+ | 命令 | 说明 |
83
+ |------|------|
84
+ | `dida init` | 初始化数据库 |
85
+ | `dida add` | 创建待办 |
86
+ | `dida ls` | 列出待办 |
87
+ | `dida get` | 获取详情 |
88
+ | `dida done` | 标记完成 |
89
+ | `dida undo` | 取消完成 |
90
+ | `dida update` | 更新待办 |
91
+ | `dida delete` | 删除待办 |
92
+ | `dida search` | 搜索待办 |
93
+ | `dida stats` | 统计信息 |
94
+
95
+ ## 数据存储
96
+
97
+ ```
98
+ ~/.dida/dida.db
99
+ ```
100
+
101
+ ## 开发
102
+
103
+ ```bash
104
+ # 克隆
105
+ git clone https://github.com/gudong/dida-cli.git
106
+ cd dida-cli
107
+
108
+ # 安装依赖
109
+ python -m venv venv
110
+ source venv/bin/activate
111
+ pip install -e ".[dev]"
112
+
113
+ # 运行测试
114
+ pytest
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,13 @@
1
+ dida/__init__.py,sha256=IJV0Ukhg2nunGc_2ToFOZUSxxM9lrgvygDaFEmQN6Rw,64
2
+ dida/cli.py,sha256=GAx2Qb4hYJ7SVTFU-Mkcv3js1WEebTuPxfzx9wXo1cQ,8597
3
+ dida/config.py,sha256=C9aRJBfaZoRO1KsZZkOAWoR0ps3AgiRdJ91_iVxMFM8,963
4
+ dida/const.py,sha256=22pPyLyG1QgnP8vA4uAyoUNh63ukahEp85W73OcpNPI,220
5
+ dida/commands/__init__.py,sha256=26xy0UOcWLZEjqdzebAmJ9V8_PnUTe8ipqe-RqT_Kf0,19
6
+ dida/db/__init__.py,sha256=GpgmZD_aQueeecbN9YmnyRMPIC7frNtvJMJGAyDouO8,469
7
+ dida/db/connection.py,sha256=Zkw-YAgCFVTIiaCRLFUFK_qnXD_SFqk8HKBxdeySwmo,779
8
+ dida/db/schema.py,sha256=oguNjBiMxY0x-yCyy0db2KIPzvomYzP19mhYDh6FSpM,1834
9
+ dida_cli-0.2.0.dist-info/METADATA,sha256=BIje8gRssquhEh__ZXuhEnMlpp_7Z8pA9cQdPjwVru0,2537
10
+ dida_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ dida_cli-0.2.0.dist-info/entry_points.txt,sha256=MQu3O92QcrQ5nbk6War7EdhFG9t51kSbttpWXX4UOJo,38
12
+ dida_cli-0.2.0.dist-info/licenses/LICENSE,sha256=yx0ZLJJzA6p8qblMHjC8z10DetflV_asazG5AEu_zZE,1063
13
+ dida_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dida = dida.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 gudong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ IN THE SOFTWARE.