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.
- memos/__init__.py +6 -0
- memos/cmds/__init__.py +0 -0
- memos/cmds/library.py +1289 -0
- memos/cmds/plugin.py +96 -0
- memos/commands.py +865 -0
- memos/config.py +225 -0
- memos/crud.py +605 -0
- memos/databases/__init__.py +0 -0
- memos/databases/initializers.py +481 -0
- memos/dataset_extractor_for_florence.py +165 -0
- memos/dataset_extractor_for_internvl2.py +192 -0
- memos/default_config.yaml +88 -0
- memos/embedding.py +129 -0
- memos/frame_extractor.py +53 -0
- memos/logging_config.py +35 -0
- memos/main.py +104 -0
- memos/migrations/alembic/README +1 -0
- memos/migrations/alembic/__pycache__/env.cpython-310.pyc +0 -0
- memos/migrations/alembic/env.py +108 -0
- memos/migrations/alembic/script.py.mako +30 -0
- memos/migrations/alembic/versions/00904ac8c6fc_add_indexes_to_entitymodel.py +63 -0
- memos/migrations/alembic/versions/04acdaf75664_add_indices_to_entitytags_and_metadata.py +86 -0
- memos/migrations/alembic/versions/12504c5b1d3c_add_extra_columns_for_embedding.py +67 -0
- memos/migrations/alembic/versions/31a1ad0e10b3_add_entity_plugin_status.py +71 -0
- memos/migrations/alembic/versions/__pycache__/00904ac8c6fc_add_indexes_to_entitymodel.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/04acdaf75664_add_indices_to_entitytags_and_metadata.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/12504c5b1d3c_add_extra_columns_for_embedding.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/20f5ecab014d_add_entity_plugin_status.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/31a1ad0e10b3_add_entity_plugin_status.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/4fcb062c5128_add_extra_columns_for_embedding.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/d10c55fbb7d2_add_index_for_entity_file_type_group_.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/__pycache__/f8f158182416_add_active_app_index.cpython-310.pyc +0 -0
- memos/migrations/alembic/versions/d10c55fbb7d2_add_index_for_entity_file_type_group_.py +44 -0
- memos/migrations/alembic/versions/f8f158182416_add_active_app_index.py +75 -0
- memos/migrations/alembic.ini +116 -0
- memos/migrations.py +19 -0
- memos/models.py +199 -0
- memos/plugins/__init__.py +0 -0
- memos/plugins/ocr/__init__.py +0 -0
- memos/plugins/ocr/main.py +251 -0
- memos/plugins/ocr/models/ch_PP-OCRv4_det_infer.onnx +0 -0
- memos/plugins/ocr/models/ch_PP-OCRv4_rec_infer.onnx +0 -0
- memos/plugins/ocr/models/ch_ppocr_mobile_v2.0_cls_train.onnx +0 -0
- memos/plugins/ocr/ppocr-gpu.yaml +43 -0
- memos/plugins/ocr/ppocr.yaml +44 -0
- memos/plugins/ocr/server.py +227 -0
- memos/plugins/ocr/temp_ppocr.yaml +42 -0
- memos/plugins/vlm/__init__.py +0 -0
- memos/plugins/vlm/main.py +251 -0
- memos/prepare_dataset.py +107 -0
- memos/process_webp.py +55 -0
- memos/read_metadata.py +32 -0
- memos/record.py +358 -0
- memos/schemas.py +289 -0
- memos/search.py +1198 -0
- memos/server.py +883 -0
- memos/shotsum.py +105 -0
- memos/shotsum_with_ocr.py +145 -0
- memos/simple_tokenizer/dict/README.md +31 -0
- memos/simple_tokenizer/dict/hmm_model.utf8 +34 -0
- memos/simple_tokenizer/dict/idf.utf8 +258826 -0
- memos/simple_tokenizer/dict/jieba.dict.utf8 +348982 -0
- memos/simple_tokenizer/dict/pos_dict/char_state_tab.utf8 +6653 -0
- memos/simple_tokenizer/dict/pos_dict/prob_emit.utf8 +166 -0
- memos/simple_tokenizer/dict/pos_dict/prob_start.utf8 +259 -0
- memos/simple_tokenizer/dict/pos_dict/prob_trans.utf8 +5222 -0
- memos/simple_tokenizer/dict/stop_words.utf8 +1534 -0
- memos/simple_tokenizer/dict/user.dict.utf8 +4 -0
- memos/simple_tokenizer/linux/libsimple.so +0 -0
- memos/simple_tokenizer/macos/libsimple.dylib +0 -0
- memos/simple_tokenizer/windows/simple.dll +0 -0
- memos/static/_app/immutable/assets/0.e250c031.css +1 -0
- memos/static/_app/immutable/assets/_layout.e7937cfe.css +1 -0
- memos/static/_app/immutable/chunks/index.5c08976b.js +1 -0
- memos/static/_app/immutable/chunks/index.60ee613b.js +4 -0
- memos/static/_app/immutable/chunks/runtime.a7926cf6.js +5 -0
- memos/static/_app/immutable/chunks/scheduler.5c1cff6e.js +1 -0
- memos/static/_app/immutable/chunks/singletons.583bdf4e.js +1 -0
- memos/static/_app/immutable/entry/app.666c1643.js +1 -0
- memos/static/_app/immutable/entry/start.aed5c701.js +3 -0
- memos/static/_app/immutable/nodes/0.5862ea38.js +7 -0
- memos/static/_app/immutable/nodes/1.35378a5e.js +1 -0
- memos/static/_app/immutable/nodes/2.1ccf9ea5.js +81 -0
- memos/static/_app/version.json +1 -0
- memos/static/app.html +36 -0
- memos/static/favicon.png +0 -0
- memos/static/logos/memos_logo_1024.png +0 -0
- memos/static/logos/memos_logo_1024@2x.png +0 -0
- memos/static/logos/memos_logo_128.png +0 -0
- memos/static/logos/memos_logo_128@2x.png +0 -0
- memos/static/logos/memos_logo_16.png +0 -0
- memos/static/logos/memos_logo_16@2x.png +0 -0
- memos/static/logos/memos_logo_256.png +0 -0
- memos/static/logos/memos_logo_256@2x.png +0 -0
- memos/static/logos/memos_logo_32.png +0 -0
- memos/static/logos/memos_logo_32@2x.png +0 -0
- memos/static/logos/memos_logo_512.png +0 -0
- memos/static/logos/memos_logo_512@2x.png +0 -0
- memos/static/logos/memos_logo_64.png +0 -0
- memos/static/logos/memos_logo_64@2x.png +0 -0
- memos/test_server.py +802 -0
- memos/utils.py +49 -0
- memos_ml_backends/florence2_server.py +176 -0
- memos_ml_backends/qwen2vl_server.py +182 -0
- memos_ml_backends/schemas.py +48 -0
- pensiev-0.25.5.dist-info/LICENSE +201 -0
- pensiev-0.25.5.dist-info/METADATA +541 -0
- pensiev-0.25.5.dist-info/RECORD +111 -0
- pensiev-0.25.5.dist-info/WHEEL +5 -0
- pensiev-0.25.5.dist-info/entry_points.txt +2 -0
- 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
|