pensiev 0.25.5__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.
Files changed (111) hide show
  1. memos/__init__.py +6 -0
  2. memos/cmds/__init__.py +0 -0
  3. memos/cmds/library.py +1289 -0
  4. memos/cmds/plugin.py +96 -0
  5. memos/commands.py +865 -0
  6. memos/config.py +225 -0
  7. memos/crud.py +605 -0
  8. memos/databases/__init__.py +0 -0
  9. memos/databases/initializers.py +481 -0
  10. memos/dataset_extractor_for_florence.py +165 -0
  11. memos/dataset_extractor_for_internvl2.py +192 -0
  12. memos/default_config.yaml +88 -0
  13. memos/embedding.py +129 -0
  14. memos/frame_extractor.py +53 -0
  15. memos/logging_config.py +35 -0
  16. memos/main.py +104 -0
  17. memos/migrations/alembic/README +1 -0
  18. memos/migrations/alembic/__pycache__/env.cpython-310.pyc +0 -0
  19. memos/migrations/alembic/env.py +108 -0
  20. memos/migrations/alembic/script.py.mako +30 -0
  21. memos/migrations/alembic/versions/00904ac8c6fc_add_indexes_to_entitymodel.py +63 -0
  22. memos/migrations/alembic/versions/04acdaf75664_add_indices_to_entitytags_and_metadata.py +86 -0
  23. memos/migrations/alembic/versions/12504c5b1d3c_add_extra_columns_for_embedding.py +67 -0
  24. memos/migrations/alembic/versions/31a1ad0e10b3_add_entity_plugin_status.py +71 -0
  25. memos/migrations/alembic/versions/__pycache__/00904ac8c6fc_add_indexes_to_entitymodel.cpython-310.pyc +0 -0
  26. memos/migrations/alembic/versions/__pycache__/04acdaf75664_add_indices_to_entitytags_and_metadata.cpython-310.pyc +0 -0
  27. memos/migrations/alembic/versions/__pycache__/12504c5b1d3c_add_extra_columns_for_embedding.cpython-310.pyc +0 -0
  28. memos/migrations/alembic/versions/__pycache__/20f5ecab014d_add_entity_plugin_status.cpython-310.pyc +0 -0
  29. memos/migrations/alembic/versions/__pycache__/31a1ad0e10b3_add_entity_plugin_status.cpython-310.pyc +0 -0
  30. memos/migrations/alembic/versions/__pycache__/4fcb062c5128_add_extra_columns_for_embedding.cpython-310.pyc +0 -0
  31. memos/migrations/alembic/versions/__pycache__/d10c55fbb7d2_add_index_for_entity_file_type_group_.cpython-310.pyc +0 -0
  32. memos/migrations/alembic/versions/__pycache__/f8f158182416_add_active_app_index.cpython-310.pyc +0 -0
  33. memos/migrations/alembic/versions/d10c55fbb7d2_add_index_for_entity_file_type_group_.py +44 -0
  34. memos/migrations/alembic/versions/f8f158182416_add_active_app_index.py +75 -0
  35. memos/migrations/alembic.ini +116 -0
  36. memos/migrations.py +19 -0
  37. memos/models.py +199 -0
  38. memos/plugins/__init__.py +0 -0
  39. memos/plugins/ocr/__init__.py +0 -0
  40. memos/plugins/ocr/main.py +251 -0
  41. memos/plugins/ocr/models/ch_PP-OCRv4_det_infer.onnx +0 -0
  42. memos/plugins/ocr/models/ch_PP-OCRv4_rec_infer.onnx +0 -0
  43. memos/plugins/ocr/models/ch_ppocr_mobile_v2.0_cls_train.onnx +0 -0
  44. memos/plugins/ocr/ppocr-gpu.yaml +43 -0
  45. memos/plugins/ocr/ppocr.yaml +44 -0
  46. memos/plugins/ocr/server.py +227 -0
  47. memos/plugins/ocr/temp_ppocr.yaml +42 -0
  48. memos/plugins/vlm/__init__.py +0 -0
  49. memos/plugins/vlm/main.py +251 -0
  50. memos/prepare_dataset.py +107 -0
  51. memos/process_webp.py +55 -0
  52. memos/read_metadata.py +32 -0
  53. memos/record.py +358 -0
  54. memos/schemas.py +289 -0
  55. memos/search.py +1198 -0
  56. memos/server.py +883 -0
  57. memos/shotsum.py +105 -0
  58. memos/shotsum_with_ocr.py +145 -0
  59. memos/simple_tokenizer/dict/README.md +31 -0
  60. memos/simple_tokenizer/dict/hmm_model.utf8 +34 -0
  61. memos/simple_tokenizer/dict/idf.utf8 +258826 -0
  62. memos/simple_tokenizer/dict/jieba.dict.utf8 +348982 -0
  63. memos/simple_tokenizer/dict/pos_dict/char_state_tab.utf8 +6653 -0
  64. memos/simple_tokenizer/dict/pos_dict/prob_emit.utf8 +166 -0
  65. memos/simple_tokenizer/dict/pos_dict/prob_start.utf8 +259 -0
  66. memos/simple_tokenizer/dict/pos_dict/prob_trans.utf8 +5222 -0
  67. memos/simple_tokenizer/dict/stop_words.utf8 +1534 -0
  68. memos/simple_tokenizer/dict/user.dict.utf8 +4 -0
  69. memos/simple_tokenizer/linux/libsimple.so +0 -0
  70. memos/simple_tokenizer/macos/libsimple.dylib +0 -0
  71. memos/simple_tokenizer/windows/simple.dll +0 -0
  72. memos/static/_app/immutable/assets/0.e250c031.css +1 -0
  73. memos/static/_app/immutable/assets/_layout.e7937cfe.css +1 -0
  74. memos/static/_app/immutable/chunks/index.5c08976b.js +1 -0
  75. memos/static/_app/immutable/chunks/index.60ee613b.js +4 -0
  76. memos/static/_app/immutable/chunks/runtime.a7926cf6.js +5 -0
  77. memos/static/_app/immutable/chunks/scheduler.5c1cff6e.js +1 -0
  78. memos/static/_app/immutable/chunks/singletons.583bdf4e.js +1 -0
  79. memos/static/_app/immutable/entry/app.666c1643.js +1 -0
  80. memos/static/_app/immutable/entry/start.aed5c701.js +3 -0
  81. memos/static/_app/immutable/nodes/0.5862ea38.js +7 -0
  82. memos/static/_app/immutable/nodes/1.35378a5e.js +1 -0
  83. memos/static/_app/immutable/nodes/2.1ccf9ea5.js +81 -0
  84. memos/static/_app/version.json +1 -0
  85. memos/static/app.html +36 -0
  86. memos/static/favicon.png +0 -0
  87. memos/static/logos/memos_logo_1024.png +0 -0
  88. memos/static/logos/memos_logo_1024@2x.png +0 -0
  89. memos/static/logos/memos_logo_128.png +0 -0
  90. memos/static/logos/memos_logo_128@2x.png +0 -0
  91. memos/static/logos/memos_logo_16.png +0 -0
  92. memos/static/logos/memos_logo_16@2x.png +0 -0
  93. memos/static/logos/memos_logo_256.png +0 -0
  94. memos/static/logos/memos_logo_256@2x.png +0 -0
  95. memos/static/logos/memos_logo_32.png +0 -0
  96. memos/static/logos/memos_logo_32@2x.png +0 -0
  97. memos/static/logos/memos_logo_512.png +0 -0
  98. memos/static/logos/memos_logo_512@2x.png +0 -0
  99. memos/static/logos/memos_logo_64.png +0 -0
  100. memos/static/logos/memos_logo_64@2x.png +0 -0
  101. memos/test_server.py +802 -0
  102. memos/utils.py +49 -0
  103. memos_ml_backends/florence2_server.py +176 -0
  104. memos_ml_backends/qwen2vl_server.py +182 -0
  105. memos_ml_backends/schemas.py +48 -0
  106. pensiev-0.25.5.dist-info/LICENSE +201 -0
  107. pensiev-0.25.5.dist-info/METADATA +541 -0
  108. pensiev-0.25.5.dist-info/RECORD +111 -0
  109. pensiev-0.25.5.dist-info/WHEEL +5 -0
  110. pensiev-0.25.5.dist-info/entry_points.txt +2 -0
  111. pensiev-0.25.5.dist-info/top_level.txt +2 -0
memos/crud.py ADDED
@@ -0,0 +1,605 @@
1
+ import logfire
2
+ from typing import List, Tuple, Optional
3
+ from sqlalchemy.orm import Session
4
+ from sqlalchemy import func, text, BigInteger
5
+ from .schemas import (
6
+ Library,
7
+ NewLibraryParam,
8
+ Folder,
9
+ NewEntityParam,
10
+ Entity,
11
+ Plugin,
12
+ NewPluginParam,
13
+ UpdateEntityParam,
14
+ NewFoldersParam,
15
+ MetadataSource,
16
+ EntityMetadataParam,
17
+ )
18
+ from .models import (
19
+ LibraryModel,
20
+ FolderModel,
21
+ EntityModel,
22
+ EntityModel,
23
+ PluginModel,
24
+ LibraryPluginModel,
25
+ TagModel,
26
+ EntityMetadataModel,
27
+ EntityTagModel,
28
+ EntityPluginStatusModel,
29
+ )
30
+ from collections import defaultdict
31
+ from .embedding import get_embeddings
32
+ import logging
33
+ from sqlite_vec import serialize_float32
34
+ import time
35
+ import json
36
+ from sqlalchemy.sql import text, bindparam
37
+ from datetime import datetime
38
+ from sqlalchemy.orm import joinedload, selectinload
39
+ from .search import create_search_provider
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def get_library_by_id(library_id: int, db: Session) -> Library | None:
45
+ return db.query(LibraryModel).filter(LibraryModel.id == library_id).first()
46
+
47
+
48
+ def create_library(library: NewLibraryParam, db: Session) -> Library:
49
+ db_library = LibraryModel(name=library.name)
50
+ db.add(db_library)
51
+ db.commit()
52
+ db.refresh(db_library)
53
+
54
+ for folder in library.folders:
55
+ db_folder = FolderModel(
56
+ path=str(folder.path),
57
+ library_id=db_library.id,
58
+ last_modified_at=folder.last_modified_at,
59
+ type=folder.type,
60
+ )
61
+ db.add(db_folder)
62
+
63
+ db.commit()
64
+ return Library(
65
+ id=db_library.id,
66
+ name=db_library.name,
67
+ folders=[
68
+ Folder(
69
+ id=db_folder.id,
70
+ path=db_folder.path,
71
+ last_modified_at=db_folder.last_modified_at,
72
+ type=db_folder.type,
73
+ )
74
+ for db_folder in db_library.folders
75
+ ],
76
+ plugins=[],
77
+ )
78
+
79
+
80
+ def get_libraries(db: Session) -> List[Library]:
81
+ return db.query(LibraryModel).order_by(LibraryModel.id.asc()).all()
82
+
83
+
84
+ def get_library_by_name(library_name: str, db: Session) -> Library | None:
85
+ return (
86
+ db.query(LibraryModel)
87
+ .filter(func.lower(LibraryModel.name) == library_name.lower())
88
+ .first()
89
+ )
90
+
91
+
92
+ def add_folders(library_id: int, folders: NewFoldersParam, db: Session) -> Library:
93
+ for folder in folders.folders:
94
+ db_folder = FolderModel(
95
+ path=str(folder.path),
96
+ library_id=library_id,
97
+ last_modified_at=folder.last_modified_at,
98
+ type=folder.type,
99
+ )
100
+ db.add(db_folder)
101
+ db.commit()
102
+ db.refresh(db_folder)
103
+
104
+ db_library = db.query(LibraryModel).filter(LibraryModel.id == library_id).first()
105
+ return Library(**db_library.__dict__)
106
+
107
+
108
+ def create_entity(
109
+ library_id: int,
110
+ entity: NewEntityParam,
111
+ db: Session,
112
+ ) -> Entity:
113
+ tags = entity.tags
114
+ metadata_entries = entity.metadata_entries
115
+
116
+ # Remove tags and metadata_entries from entity
117
+ entity.tags = None
118
+ entity.metadata_entries = None
119
+
120
+ db_entity = EntityModel(
121
+ **entity.model_dump(exclude_none=True), library_id=library_id
122
+ )
123
+ db.add(db_entity)
124
+ db.commit()
125
+ db.refresh(db_entity)
126
+
127
+ # Handle tags separately
128
+ if tags:
129
+ for tag_name in tags:
130
+ tag = db.query(TagModel).filter(TagModel.name == tag_name).first()
131
+ if not tag:
132
+ tag = TagModel(name=tag_name)
133
+ db.add(tag)
134
+ db.commit()
135
+ db.refresh(tag)
136
+ entity_tag = EntityTagModel(
137
+ entity_id=db_entity.id,
138
+ tag_id=tag.id,
139
+ source=MetadataSource.PLUGIN_GENERATED,
140
+ )
141
+ db.add(entity_tag)
142
+ db.commit()
143
+
144
+ # Handle attrs separately
145
+ if metadata_entries:
146
+ for attr in metadata_entries:
147
+ entity_metadata = EntityMetadataModel(
148
+ entity_id=db_entity.id,
149
+ key=attr.key,
150
+ value=attr.value,
151
+ source=attr.source,
152
+ source_type=MetadataSource.PLUGIN_GENERATED if attr.source else None,
153
+ data_type=attr.data_type,
154
+ )
155
+ db.add(entity_metadata)
156
+ db.commit()
157
+ db.refresh(db_entity)
158
+
159
+ return Entity(**db_entity.__dict__)
160
+
161
+
162
+ def get_entity_by_id(entity_id: int, db: Session) -> Entity | None:
163
+ return db.query(EntityModel).filter(EntityModel.id == entity_id).first()
164
+
165
+
166
+ def get_entities_of_folder(
167
+ library_id: int,
168
+ folder_id: int,
169
+ db: Session,
170
+ limit: int = 10,
171
+ offset: int = 0,
172
+ path_prefix: str | None = None,
173
+ ) -> Tuple[List[Entity], int]:
174
+ # First get the entity IDs with limit and offset
175
+ id_query = (
176
+ db.query(EntityModel.id)
177
+ .filter(
178
+ EntityModel.folder_id == folder_id,
179
+ EntityModel.library_id == library_id,
180
+ )
181
+ .order_by(EntityModel.file_last_modified_at.asc())
182
+ )
183
+
184
+ # Add path_prefix filter if provided
185
+ if path_prefix:
186
+ id_query = id_query.filter(EntityModel.filepath.like(f"{path_prefix}%"))
187
+
188
+ total_count = id_query.count()
189
+ entity_ids = id_query.limit(limit).offset(offset).all()
190
+ entity_ids = [id[0] for id in entity_ids]
191
+
192
+ # Then get the full entities with relationships for those IDs
193
+ entities = (
194
+ db.query(EntityModel)
195
+ .options(
196
+ joinedload(EntityModel.metadata_entries),
197
+ joinedload(EntityModel.tags),
198
+ joinedload(EntityModel.plugin_status)
199
+ )
200
+ .filter(EntityModel.id.in_(entity_ids))
201
+ .order_by(EntityModel.file_last_modified_at.asc())
202
+ .all()
203
+ )
204
+
205
+ return entities, total_count
206
+
207
+
208
+ def get_entity_by_filepath(filepath: str, db: Session) -> Entity | None:
209
+ return db.query(EntityModel).filter(EntityModel.filepath == filepath).first()
210
+
211
+
212
+ def get_entities_by_filepaths(filepaths: List[str], db: Session) -> List[Entity]:
213
+ return (
214
+ db.query(EntityModel)
215
+ .options(
216
+ joinedload(EntityModel.metadata_entries),
217
+ joinedload(EntityModel.tags),
218
+ joinedload(EntityModel.plugin_status),
219
+ )
220
+ .filter(EntityModel.filepath.in_(filepaths))
221
+ .all()
222
+ )
223
+
224
+
225
+ def remove_entity(entity_id: int, db: Session):
226
+ entity = db.query(EntityModel).filter(EntityModel.id == entity_id).first()
227
+ if entity:
228
+ # Delete the entity from FTS and vec tables first
229
+ db.execute(text("DELETE FROM entities_fts WHERE id = :id"), {"id": entity_id})
230
+ db.execute(
231
+ text("DELETE FROM entities_vec_v2 WHERE rowid = :id"), {"id": entity_id}
232
+ )
233
+
234
+ # Then delete the entity itself
235
+ db.delete(entity)
236
+ db.commit()
237
+ else:
238
+ raise ValueError(f"Entity with id {entity_id} not found")
239
+
240
+
241
+ def create_plugin(newPlugin: NewPluginParam, db: Session) -> Plugin:
242
+ db_plugin = PluginModel(**newPlugin.model_dump(mode="json"))
243
+ db.add(db_plugin)
244
+ db.commit()
245
+ db.refresh(db_plugin)
246
+ return db_plugin
247
+
248
+
249
+ def get_plugins(db: Session) -> List[Plugin]:
250
+ return db.query(PluginModel).order_by(PluginModel.id.asc()).all()
251
+
252
+
253
+ def get_plugin_by_name(plugin_name: str, db: Session) -> Plugin | None:
254
+ return (
255
+ db.query(PluginModel)
256
+ .filter(func.lower(PluginModel.name) == plugin_name.lower())
257
+ .first()
258
+ )
259
+
260
+
261
+ def add_plugin_to_library(library_id: int, plugin_id: int, db: Session):
262
+ library_plugin = LibraryPluginModel(library_id=library_id, plugin_id=plugin_id)
263
+ db.add(library_plugin)
264
+ db.commit()
265
+ db.refresh(library_plugin)
266
+
267
+
268
+ def find_entities_by_ids(entity_ids: List[int], db: Session) -> List[Entity]:
269
+ db_entities = (
270
+ db.query(EntityModel)
271
+ .options(joinedload(EntityModel.metadata_entries), joinedload(EntityModel.tags))
272
+ .filter(EntityModel.id.in_(entity_ids))
273
+ .all()
274
+ )
275
+ return [Entity(**entity.__dict__) for entity in db_entities]
276
+
277
+
278
+ def update_entity(
279
+ entity_id: int,
280
+ updated_entity: UpdateEntityParam,
281
+ db: Session,
282
+ ) -> Entity:
283
+ db_entity = db.query(EntityModel).filter(EntityModel.id == entity_id).first()
284
+
285
+ if db_entity is None:
286
+ raise ValueError(f"Entity with id {entity_id} not found")
287
+
288
+ # Update the main fields of the entity
289
+ for key, value in updated_entity.model_dump().items():
290
+ if key not in ["tags", "metadata_entries"] and value is not None:
291
+ setattr(db_entity, key, value)
292
+
293
+ # Handle tags separately
294
+ if updated_entity.tags is not None:
295
+ # Clear existing tags
296
+ db.query(EntityTagModel).filter(EntityTagModel.entity_id == entity_id).delete()
297
+ db.commit()
298
+
299
+ for tag_name in updated_entity.tags:
300
+ tag = db.query(TagModel).filter(TagModel.name == tag_name).first()
301
+ if not tag:
302
+ tag = TagModel(name=tag_name)
303
+ db.add(tag)
304
+ db.commit()
305
+ db.refresh(tag)
306
+ entity_tag = EntityTagModel(
307
+ entity_id=db_entity.id,
308
+ tag_id=tag.id,
309
+ source=MetadataSource.PLUGIN_GENERATED,
310
+ )
311
+ db.add(entity_tag)
312
+ db.commit()
313
+
314
+ # Handle attrs separately
315
+ if updated_entity.metadata_entries is not None:
316
+ # Clear existing attrs
317
+ db.query(EntityMetadataModel).filter(
318
+ EntityMetadataModel.entity_id == entity_id
319
+ ).delete()
320
+ db.commit()
321
+
322
+ for attr in updated_entity.metadata_entries:
323
+ entity_metadata = EntityMetadataModel(
324
+ entity_id=db_entity.id,
325
+ key=attr.key,
326
+ value=attr.value,
327
+ source=attr.source if attr.source is not None else None,
328
+ source_type=(
329
+ MetadataSource.PLUGIN_GENERATED if attr.source is not None else None
330
+ ),
331
+ data_type=attr.data_type,
332
+ )
333
+ db.add(entity_metadata)
334
+ db_entity.metadata_entries.append(entity_metadata)
335
+
336
+ db.commit()
337
+ db.refresh(db_entity)
338
+
339
+ return Entity(**db_entity.__dict__)
340
+
341
+
342
+ def touch_entity(entity_id: int, db: Session) -> bool:
343
+ db_entity = db.query(EntityModel).filter(EntityModel.id == entity_id).first()
344
+ if db_entity:
345
+ db_entity.last_scan_at = func.now()
346
+ db.commit()
347
+ db.refresh(db_entity)
348
+ return True
349
+ else:
350
+ return False
351
+
352
+
353
+ def update_entity_tags(
354
+ entity_id: int,
355
+ tags: List[str],
356
+ db: Session,
357
+ ) -> Entity:
358
+ db_entity = get_entity_by_id(entity_id, db)
359
+ if not db_entity:
360
+ raise ValueError(f"Entity with id {entity_id} not found")
361
+
362
+ # Clear existing tags
363
+ db.query(EntityTagModel).filter(EntityTagModel.entity_id == entity_id).delete()
364
+
365
+ for tag_name in tags:
366
+ tag = db.query(TagModel).filter(TagModel.name == tag_name).first()
367
+ if not tag:
368
+ tag = TagModel(name=tag_name)
369
+ db.add(tag)
370
+ db.commit()
371
+ db.refresh(tag)
372
+ entity_tag = EntityTagModel(
373
+ entity_id=db_entity.id,
374
+ tag_id=tag.id,
375
+ source=MetadataSource.PLUGIN_GENERATED,
376
+ )
377
+ db.add(entity_tag)
378
+
379
+ # Update last_scan_at in the same transaction
380
+ db_entity.last_scan_at = func.now()
381
+
382
+ db.commit()
383
+ db.refresh(db_entity)
384
+
385
+ return Entity(**db_entity.__dict__)
386
+
387
+
388
+ def add_new_tags(entity_id: int, tags: List[str], db: Session) -> Entity:
389
+ db_entity = get_entity_by_id(entity_id, db)
390
+ if not db_entity:
391
+ raise ValueError(f"Entity with id {entity_id} not found")
392
+
393
+ existing_tags = set(tag.name for tag in db_entity.tags)
394
+ new_tags = set(tags) - existing_tags
395
+
396
+ for tag_name in new_tags:
397
+ tag = db.query(TagModel).filter(TagModel.name == tag_name).first()
398
+ if not tag:
399
+ tag = TagModel(name=tag_name)
400
+ db.add(tag)
401
+ db.commit()
402
+ db.refresh(tag)
403
+ entity_tag = EntityTagModel(
404
+ entity_id=db_entity.id,
405
+ tag_id=tag.id,
406
+ source=MetadataSource.PLUGIN_GENERATED,
407
+ )
408
+ db.add(entity_tag)
409
+
410
+ # Update last_scan_at in the same transaction
411
+ db_entity.last_scan_at = func.now()
412
+
413
+ db.commit()
414
+ db.refresh(db_entity)
415
+
416
+ return Entity(**db_entity.__dict__)
417
+
418
+
419
+ def update_entity_metadata_entries(
420
+ entity_id: int,
421
+ updated_metadata: List[EntityMetadataParam],
422
+ db: Session,
423
+ ) -> Entity:
424
+ db_entity = get_entity_by_id(entity_id, db)
425
+
426
+ existing_metadata_entries = (
427
+ db.query(EntityMetadataModel)
428
+ .filter(EntityMetadataModel.entity_id == db_entity.id)
429
+ .all()
430
+ )
431
+
432
+ existing_metadata_dict = {entry.key: entry for entry in existing_metadata_entries}
433
+
434
+ for metadata in updated_metadata:
435
+ if metadata.key in existing_metadata_dict:
436
+ existing_metadata = existing_metadata_dict[metadata.key]
437
+ existing_metadata.value = metadata.value
438
+ existing_metadata.source = (
439
+ metadata.source
440
+ if metadata.source is not None
441
+ else existing_metadata.source
442
+ )
443
+ existing_metadata.source_type = (
444
+ MetadataSource.PLUGIN_GENERATED
445
+ if metadata.source is not None
446
+ else existing_metadata.source_type
447
+ )
448
+ existing_metadata.data_type = metadata.data_type
449
+ else:
450
+ entity_metadata = EntityMetadataModel(
451
+ entity_id=db_entity.id,
452
+ key=metadata.key,
453
+ value=metadata.value,
454
+ source=metadata.source if metadata.source is not None else None,
455
+ source_type=(
456
+ MetadataSource.PLUGIN_GENERATED
457
+ if metadata.source is not None
458
+ else None
459
+ ),
460
+ data_type=metadata.data_type,
461
+ )
462
+ db.add(entity_metadata)
463
+ db_entity.metadata_entries.append(entity_metadata)
464
+
465
+ # Update last_scan_at in the same transaction
466
+ db_entity.last_scan_at = func.now()
467
+
468
+ db.commit()
469
+ db.refresh(db_entity)
470
+
471
+ return Entity(**db_entity.__dict__)
472
+
473
+
474
+ def get_plugin_by_id(plugin_id: int, db: Session) -> Plugin | None:
475
+ return db.query(PluginModel).filter(PluginModel.id == plugin_id).first()
476
+
477
+
478
+ def remove_plugin_from_library(library_id: int, plugin_id: int, db: Session):
479
+ library_plugin = (
480
+ db.query(LibraryPluginModel)
481
+ .filter(
482
+ LibraryPluginModel.library_id == library_id,
483
+ LibraryPluginModel.plugin_id == plugin_id,
484
+ )
485
+ .first()
486
+ )
487
+
488
+ if library_plugin:
489
+ db.delete(library_plugin)
490
+ db.commit()
491
+ else:
492
+ raise ValueError(f"Plugin {plugin_id} not found in library {library_id}")
493
+
494
+
495
+ def list_entities(
496
+ db: Session,
497
+ limit: int = 200,
498
+ library_ids: Optional[List[int]] = None,
499
+ start: Optional[int] = None,
500
+ end: Optional[int] = None,
501
+ ) -> List[Entity]:
502
+ query = (
503
+ db.query(EntityModel)
504
+ .options(joinedload(EntityModel.metadata_entries), joinedload(EntityModel.tags))
505
+ .filter(EntityModel.file_type_group == "image")
506
+ )
507
+
508
+ if library_ids:
509
+ query = query.filter(EntityModel.library_id.in_(library_ids))
510
+
511
+ if start is not None and end is not None:
512
+ # Convert timestamp to Unix timestamp using EXTRACT(EPOCH FROM timestamp)
513
+ # This works in both PostgreSQL and SQLite (SQLite will use its own implementation)
514
+ query = query.filter(
515
+ func.extract('epoch', EntityModel.file_created_at).cast(BigInteger).between(start, end)
516
+ )
517
+
518
+ entities = query.order_by(EntityModel.file_created_at.desc()).limit(limit).all()
519
+
520
+ return [Entity(**entity.__dict__) for entity in entities]
521
+
522
+
523
+ def get_entity_context(
524
+ db: Session, library_id: int, entity_id: int, prev: int = 0, next: int = 0
525
+ ) -> Tuple[List[Entity], List[Entity]]:
526
+ """
527
+ Get the context (previous and next entities) for a given entity.
528
+ Returns a tuple of (previous_entities, next_entities).
529
+ """
530
+ # First get the target entity to get its timestamp
531
+ target_entity = (
532
+ db.query(EntityModel)
533
+ .filter(
534
+ EntityModel.id == entity_id,
535
+ EntityModel.library_id == library_id,
536
+ )
537
+ .first()
538
+ )
539
+
540
+ if not target_entity:
541
+ return [], []
542
+
543
+ # Get previous entities
544
+ prev_entities = []
545
+ if prev > 0:
546
+ prev_entities = (
547
+ db.query(EntityModel)
548
+ .filter(
549
+ EntityModel.library_id == library_id,
550
+ EntityModel.file_created_at < target_entity.file_created_at,
551
+ )
552
+ .order_by(EntityModel.file_created_at.desc())
553
+ .limit(prev)
554
+ .all()
555
+ )
556
+ # Reverse the list to get chronological order and convert to Entity models
557
+ prev_entities = [Entity(**entity.__dict__) for entity in prev_entities][::-1]
558
+
559
+ # Get next entities
560
+ next_entities = []
561
+ if next > 0:
562
+ next_entities = (
563
+ db.query(EntityModel)
564
+ .filter(
565
+ EntityModel.library_id == library_id,
566
+ EntityModel.file_created_at > target_entity.file_created_at,
567
+ )
568
+ .order_by(EntityModel.file_created_at.asc())
569
+ .limit(next)
570
+ .all()
571
+ )
572
+ # Convert to Entity models
573
+ next_entities = [Entity(**entity.__dict__) for entity in next_entities]
574
+
575
+ return prev_entities, next_entities
576
+
577
+
578
+ def record_plugin_processed(entity_id: int, plugin_id: int, db: Session):
579
+ """Record that an entity has been processed by a plugin"""
580
+ status = EntityPluginStatusModel(entity_id=entity_id, plugin_id=plugin_id)
581
+ db.merge(status) # merge will insert or update
582
+ db.commit()
583
+
584
+
585
+ def get_pending_plugins(entity_id: int, library_id: int, db: Session) -> List[int]:
586
+ """Get list of plugin IDs that haven't processed this entity yet"""
587
+ # Get all plugins associated with the library
588
+ library_plugins = (
589
+ db.query(PluginModel.id)
590
+ .join(LibraryPluginModel)
591
+ .filter(LibraryPluginModel.library_id == library_id)
592
+ .all()
593
+ )
594
+ library_plugin_ids = [p.id for p in library_plugins]
595
+
596
+ # Get plugins that have already processed this entity
597
+ processed_plugins = (
598
+ db.query(EntityPluginStatusModel.plugin_id)
599
+ .filter(EntityPluginStatusModel.entity_id == entity_id)
600
+ .all()
601
+ )
602
+ processed_plugin_ids = [p.plugin_id for p in processed_plugins]
603
+
604
+ # Return plugins that need to process this entity
605
+ return list(set(library_plugin_ids) - set(processed_plugin_ids))
File without changes