basic-memory 0.7.0__py3-none-any.whl → 0.17.4__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 basic-memory might be problematic. Click here for more details.

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,310 @@
1
+ """Directory service for managing file directories and tree structure."""
2
+
3
+ import fnmatch
4
+ import logging
5
+ import os
6
+ from datetime import datetime
7
+ from typing import Dict, List, Optional, Sequence
8
+
9
+
10
+ from basic_memory.models import Entity
11
+ from basic_memory.repository import EntityRepository
12
+ from basic_memory.schemas.directory import DirectoryNode
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _mtime_to_datetime(entity: Entity) -> datetime:
18
+ """Convert entity mtime (file modification time) to datetime.
19
+
20
+ Returns the file's actual modification time, falling back to updated_at
21
+ if mtime is not available.
22
+ """
23
+ if entity.mtime: # pragma: no cover
24
+ return datetime.fromtimestamp(entity.mtime).astimezone() # pragma: no cover
25
+ return entity.updated_at
26
+
27
+
28
+ class DirectoryService:
29
+ """Service for working with directory trees."""
30
+
31
+ def __init__(self, entity_repository: EntityRepository):
32
+ """Initialize the directory service.
33
+
34
+ Args:
35
+ entity_repository: Directory repository for data access.
36
+ """
37
+ self.entity_repository = entity_repository
38
+
39
+ async def get_directory_tree(self) -> DirectoryNode:
40
+ """Build a hierarchical directory tree from indexed files."""
41
+
42
+ # Get all files from DB (flat list)
43
+ entity_rows = await self.entity_repository.find_all()
44
+
45
+ # Create a root directory node
46
+ root_node = DirectoryNode(name="Root", directory_path="/", type="directory")
47
+
48
+ # Map to store directory nodes by path for easy lookup
49
+ dir_map: Dict[str, DirectoryNode] = {root_node.directory_path: root_node}
50
+
51
+ # First pass: create all directory nodes
52
+ for file in entity_rows:
53
+ # Process directory path components
54
+ parts = [p for p in file.file_path.split("/") if p]
55
+
56
+ # Create directory structure
57
+ current_path = "/"
58
+ for i, part in enumerate(parts[:-1]): # Skip the filename
59
+ parent_path = current_path
60
+ # Build the directory path
61
+ current_path = (
62
+ f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
63
+ )
64
+
65
+ # Create directory node if it doesn't exist
66
+ if current_path not in dir_map:
67
+ dir_node = DirectoryNode(
68
+ name=part, directory_path=current_path, type="directory"
69
+ )
70
+ dir_map[current_path] = dir_node
71
+
72
+ # Add to parent's children
73
+ if parent_path in dir_map:
74
+ dir_map[parent_path].children.append(dir_node)
75
+
76
+ # Second pass: add file nodes to their parent directories
77
+ for file in entity_rows:
78
+ file_name = os.path.basename(file.file_path)
79
+ parent_dir = os.path.dirname(file.file_path)
80
+ directory_path = "/" if parent_dir == "" else f"/{parent_dir}"
81
+
82
+ # Create file node
83
+ file_node = DirectoryNode(
84
+ name=file_name,
85
+ file_path=file.file_path, # Original path from DB (no leading slash)
86
+ directory_path=f"/{file.file_path}", # Path with leading slash
87
+ type="file",
88
+ title=file.title,
89
+ permalink=file.permalink,
90
+ external_id=file.external_id, # UUID for v2 API
91
+ entity_id=file.id,
92
+ entity_type=file.entity_type,
93
+ content_type=file.content_type,
94
+ updated_at=_mtime_to_datetime(file),
95
+ )
96
+
97
+ # Add to parent directory's children
98
+ if directory_path in dir_map:
99
+ dir_map[directory_path].children.append(file_node)
100
+ else:
101
+ # If parent directory doesn't exist (should be rare), add to root
102
+ dir_map["/"].children.append(file_node) # pragma: no cover
103
+
104
+ # Return the root node with its children
105
+ return root_node
106
+
107
+ async def get_directory_structure(self) -> DirectoryNode:
108
+ """Build a hierarchical directory structure without file details.
109
+
110
+ Optimized method for folder navigation that only returns directory nodes,
111
+ no file metadata. Much faster than get_directory_tree() for large knowledge bases.
112
+
113
+ Returns:
114
+ DirectoryNode tree containing only folders (type="directory")
115
+ """
116
+ # Get unique directories without loading entities
117
+ directories = await self.entity_repository.get_distinct_directories()
118
+
119
+ # Create a root directory node
120
+ root_node = DirectoryNode(name="Root", directory_path="/", type="directory")
121
+
122
+ # Map to store directory nodes by path for easy lookup
123
+ dir_map: Dict[str, DirectoryNode] = {"/": root_node}
124
+
125
+ # Build tree with just folders
126
+ for dir_path in directories:
127
+ parts = [p for p in dir_path.split("/") if p]
128
+ current_path = "/"
129
+
130
+ for i, part in enumerate(parts):
131
+ parent_path = current_path
132
+ # Build the directory path
133
+ current_path = (
134
+ f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
135
+ )
136
+
137
+ # Create directory node if it doesn't exist
138
+ if current_path not in dir_map:
139
+ dir_node = DirectoryNode(
140
+ name=part, directory_path=current_path, type="directory"
141
+ )
142
+ dir_map[current_path] = dir_node
143
+
144
+ # Add to parent's children
145
+ if parent_path in dir_map:
146
+ dir_map[parent_path].children.append(dir_node)
147
+
148
+ return root_node
149
+
150
+ async def list_directory(
151
+ self,
152
+ dir_name: str = "/",
153
+ depth: int = 1,
154
+ file_name_glob: Optional[str] = None,
155
+ ) -> List[DirectoryNode]:
156
+ """List directory contents with filtering and depth control.
157
+
158
+ Args:
159
+ dir_name: Directory path to list (default: root "/")
160
+ depth: Recursion depth (1 = immediate children only)
161
+ file_name_glob: Glob pattern for filtering file names
162
+
163
+ Returns:
164
+ List of DirectoryNode objects matching the criteria
165
+ """
166
+ # Normalize directory path
167
+ # Strip ./ prefix if present (handles relative path notation)
168
+ if dir_name.startswith("./"):
169
+ dir_name = dir_name[2:] # Remove "./" prefix
170
+
171
+ # Ensure path starts with "/"
172
+ if not dir_name.startswith("/"):
173
+ dir_name = f"/{dir_name}"
174
+
175
+ # Remove trailing slashes except for root
176
+ if dir_name != "/" and dir_name.endswith("/"):
177
+ dir_name = dir_name.rstrip("/")
178
+
179
+ # Optimize: Query only entities in the target directory
180
+ # instead of loading the entire tree
181
+ dir_prefix = dir_name.lstrip("/")
182
+ entity_rows = await self.entity_repository.find_by_directory_prefix(dir_prefix)
183
+
184
+ # Build a partial tree from only the relevant entities
185
+ root_tree = self._build_directory_tree_from_entities(entity_rows, dir_name)
186
+
187
+ # Find the target directory node
188
+ target_node = self._find_directory_node(root_tree, dir_name)
189
+ if not target_node:
190
+ return [] # pragma: no cover
191
+
192
+ # Collect nodes with depth and glob filtering
193
+ result = []
194
+ self._collect_nodes_recursive(target_node, result, depth, file_name_glob, 0)
195
+
196
+ return result
197
+
198
+ def _build_directory_tree_from_entities(
199
+ self, entity_rows: Sequence[Entity], root_path: str
200
+ ) -> DirectoryNode:
201
+ """Build a directory tree from a subset of entities.
202
+
203
+ Args:
204
+ entity_rows: Sequence of entity objects to build tree from
205
+ root_path: Root directory path for the tree
206
+
207
+ Returns:
208
+ DirectoryNode representing the tree root
209
+ """
210
+ # Create a root directory node
211
+ root_node = DirectoryNode(name="Root", directory_path=root_path, type="directory")
212
+
213
+ # Map to store directory nodes by path for easy lookup
214
+ dir_map: Dict[str, DirectoryNode] = {root_path: root_node}
215
+
216
+ # First pass: create all directory nodes
217
+ for file in entity_rows:
218
+ # Process directory path components
219
+ parts = [p for p in file.file_path.split("/") if p]
220
+
221
+ # Create directory structure
222
+ current_path = "/"
223
+ for i, part in enumerate(parts[:-1]): # Skip the filename
224
+ parent_path = current_path
225
+ # Build the directory path
226
+ current_path = (
227
+ f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
228
+ )
229
+
230
+ # Create directory node if it doesn't exist
231
+ if current_path not in dir_map:
232
+ dir_node = DirectoryNode(
233
+ name=part, directory_path=current_path, type="directory"
234
+ )
235
+ dir_map[current_path] = dir_node
236
+
237
+ # Add to parent's children
238
+ if parent_path in dir_map:
239
+ dir_map[parent_path].children.append(dir_node)
240
+
241
+ # Second pass: add file nodes to their parent directories
242
+ for file in entity_rows:
243
+ file_name = os.path.basename(file.file_path)
244
+ parent_dir = os.path.dirname(file.file_path)
245
+ directory_path = "/" if parent_dir == "" else f"/{parent_dir}"
246
+
247
+ # Create file node
248
+ file_node = DirectoryNode(
249
+ name=file_name,
250
+ file_path=file.file_path,
251
+ directory_path=f"/{file.file_path}",
252
+ type="file",
253
+ title=file.title,
254
+ permalink=file.permalink,
255
+ external_id=file.external_id, # UUID for v2 API
256
+ entity_id=file.id,
257
+ entity_type=file.entity_type,
258
+ content_type=file.content_type,
259
+ updated_at=_mtime_to_datetime(file),
260
+ )
261
+
262
+ # Add to parent directory's children
263
+ if directory_path in dir_map:
264
+ dir_map[directory_path].children.append(file_node)
265
+ elif root_path in dir_map: # pragma: no cover
266
+ # Fallback to root if parent not found
267
+ dir_map[root_path].children.append(file_node) # pragma: no cover
268
+
269
+ return root_node
270
+
271
+ def _find_directory_node(
272
+ self, root: DirectoryNode, target_path: str
273
+ ) -> Optional[DirectoryNode]:
274
+ """Find a directory node by path in the tree."""
275
+ if root.directory_path == target_path:
276
+ return root
277
+
278
+ for child in root.children: # pragma: no cover
279
+ if child.type == "directory": # pragma: no cover
280
+ found = self._find_directory_node(child, target_path) # pragma: no cover
281
+ if found: # pragma: no cover
282
+ return found # pragma: no cover
283
+
284
+ return None # pragma: no cover
285
+
286
+ def _collect_nodes_recursive(
287
+ self,
288
+ node: DirectoryNode,
289
+ result: List[DirectoryNode],
290
+ max_depth: int,
291
+ file_name_glob: Optional[str],
292
+ current_depth: int,
293
+ ) -> None:
294
+ """Recursively collect nodes with depth and glob filtering."""
295
+ if current_depth >= max_depth:
296
+ return
297
+
298
+ for child in node.children:
299
+ # Apply glob filtering
300
+ if file_name_glob and not fnmatch.fnmatch(child.name, file_name_glob):
301
+ continue
302
+
303
+ # Add the child to results
304
+ result.append(child)
305
+
306
+ # Recurse into subdirectories if we haven't reached max depth
307
+ if child.type == "directory" and current_depth < max_depth:
308
+ self._collect_nodes_recursive(
309
+ child, result, max_depth, file_name_glob, current_depth + 1
310
+ )