panoptic 0.0.9.dev10__tar.gz → 0.1.0__tar.gz

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 (46) hide show
  1. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/PKG-INFO +1 -1
  2. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/api.py +70 -28
  3. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/compute/similarity.py +3 -3
  4. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/compute/transform.py +1 -1
  5. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/compute/utils.py +3 -2
  6. panoptic-0.1.0/panoptic/core/__init__.py +266 -0
  7. panoptic-0.1.0/panoptic/core/db.py +376 -0
  8. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/core/db_utils.py +2 -2
  9. panoptic-0.1.0/panoptic/core/image_importer.py +113 -0
  10. panoptic-0.1.0/panoptic/core/process_queue.py +145 -0
  11. panoptic-0.1.0/panoptic/html/assets/index-2b4b1df5.css +6 -0
  12. panoptic-0.1.0/panoptic/html/assets/index-9aba2054.js +22 -0
  13. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/html/index.html +2 -2
  14. panoptic-0.1.0/panoptic/main.py +269 -0
  15. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/models/models.py +32 -22
  16. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/models/payloads.py +14 -6
  17. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/scripts/create_db.sql +26 -11
  18. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/scripts/populate_db.sql +5 -5
  19. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/scripts/to_pca.py +6 -6
  20. panoptic-0.1.0/panoptic/utils.py +24 -0
  21. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic.egg-info/PKG-INFO +1 -1
  22. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic.egg-info/SOURCES.txt +4 -2
  23. panoptic-0.0.9.dev10/requirements.txt → panoptic-0.1.0/panoptic.egg-info/requires.txt +3 -2
  24. panoptic-0.0.9.dev10/panoptic.egg-info/requires.txt → panoptic-0.1.0/requirements.txt +2 -1
  25. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/setup.py +1 -1
  26. panoptic-0.0.9.dev10/panoptic/core/__init__.py +0 -253
  27. panoptic-0.0.9.dev10/panoptic/core/db.py +0 -306
  28. panoptic-0.0.9.dev10/panoptic/core/image_importer.py +0 -139
  29. panoptic-0.0.9.dev10/panoptic/html/assets/index-5bc6c5ef.js +0 -21
  30. panoptic-0.0.9.dev10/panoptic/html/assets/index-be3f5969.css +0 -6
  31. panoptic-0.0.9.dev10/panoptic/main.py +0 -135
  32. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/build_commans.txt +0 -0
  33. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/description.md +0 -0
  34. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/__init__.py +0 -0
  35. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/compute/__init__.py +0 -0
  36. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/compute/ocr.py +0 -0
  37. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/html/assets/bootstrap-icons-966620f9.woff2 +0 -0
  38. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/html/assets/bootstrap-icons-c6569d46.woff +0 -0
  39. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/html/favicon.ico +0 -0
  40. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/models/__init__.py +0 -0
  41. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic/scripts/__init__.py +0 -0
  42. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic.egg-info/dependency_links.txt +0 -0
  43. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic.egg-info/entry_points.txt +0 -0
  44. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/panoptic.egg-info/top_level.txt +0 -0
  45. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/setup.cfg +0 -0
  46. {panoptic-0.0.9.dev10 → panoptic-0.1.0}/thunder-collection_Panoptic.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panoptic
3
- Version: 0.0.9.dev10
3
+ Version: 0.1.0
4
4
  License: MPL-2.0 license
5
5
  Description-Content-Type: text/markdown
6
6
 
@@ -1,25 +1,27 @@
1
+ import logging
1
2
  import os
2
3
  from sys import platform
3
4
  from typing import Optional
4
5
 
5
6
  import aiofiles as aiofiles
7
+ import pandas as pd
6
8
  from fastapi import FastAPI, Query, UploadFile
7
9
  from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import ORJSONResponse
8
11
  from pydantic import BaseModel
9
12
  from starlette.responses import Response
10
13
  from starlette.staticfiles import StaticFiles
11
- import pandas as pd
12
14
 
13
15
  from panoptic import core
14
- from panoptic.scripts.to_pca import compute_all_pca
15
- from panoptic.core import create_property, add_property_to_images, get_images, create_tag, \
16
- delete_image_property, \
16
+ from panoptic.core import create_property, create_tag, \
17
17
  update_tag, get_tags, get_properties, delete_property, update_property, delete_tag, delete_tag_parent, add_folder, \
18
- db_utils, make_clusters, get_similar_images, get_full_image, read_properties_file
18
+ db_utils, make_clusters, get_similar_images, read_properties_file, get_full_images, set_property_values
19
19
  from panoptic.core import db
20
- from panoptic.models import Property, Images, Tag, Tags, Properties, PropertyPayload, \
21
- AddImagePropertyPayload, AddTagPayload, DeleteImagePropertyPayload, \
22
- UpdateTagPayload, UpdatePropertyPayload, Tab, MakeClusterPayload
20
+ from panoptic.models import Property, Tag, Tags, Properties, PropertyPayload, \
21
+ SetPropertyValuePayload, AddTagPayload, DeleteImagePropertyPayload, \
22
+ UpdateTagPayload, UpdatePropertyPayload, Tab, MakeClusterPayload, PropertyValue, GetSimilarImagesPayload, \
23
+ ChangeProjectPayload
24
+ from panoptic.scripts.to_pca import compute_all_pca
23
25
 
24
26
  app = FastAPI()
25
27
  app.add_middleware(
@@ -42,7 +44,7 @@ async def startup_event():
42
44
  # Route pour créer une property et l'insérer dans la table des properties
43
45
  @app.post("/property")
44
46
  async def create_property_route(payload: PropertyPayload) -> Property:
45
- return await create_property(payload.name, payload.type)
47
+ return await create_property(payload.name, payload.type, payload.mode)
46
48
 
47
49
 
48
50
  @app.get("/property")
@@ -54,22 +56,24 @@ async def get_properties_route() -> Properties:
54
56
  async def update_property_route(payload: UpdatePropertyPayload) -> Property:
55
57
  return await update_property(payload)
56
58
 
59
+
57
60
  @app.post('/property/file')
58
61
  async def properties_by_file(file: UploadFile):
59
62
  data = pd.read_csv(file.file, sep=";")
60
63
  return await read_properties_file(data)
61
64
 
65
+
62
66
  @app.delete('/property/{property_id}')
63
67
  async def delete_property_route(property_id: str):
64
68
  await delete_property(property_id)
65
- return f"Property {property_id} correctly deleted"
69
+ return await get_properties()
66
70
 
67
71
 
68
72
  # Route pour récupérer la liste de toutes les images
69
- @app.get("/images")
73
+ @app.get("/images", response_class=ORJSONResponse)
70
74
  async def get_images_route():
71
- images = await get_images()
72
- return images
75
+ images = await get_full_images()
76
+ return ORJSONResponse(images)
73
77
 
74
78
 
75
79
  @app.get('/images/{file_path:path}')
@@ -90,15 +94,15 @@ async def get_image(file_path: str):
90
94
  # Route pour ajouter une property à une image dans la table de jointure entre image et property
91
95
  # On retourne le payload pour pouvoir valider l'update côté front
92
96
  @app.post("/image_property")
93
- async def add_image_property(payload: AddImagePropertyPayload) -> AddImagePropertyPayload:
94
- await add_property_to_images(payload.property_id, payload.sha1_list, payload.value)
95
- return payload
97
+ async def add_image_property(payload: SetPropertyValuePayload):
98
+ updated, value = await set_property_values(property_id=payload.property_id, image_ids=payload.image_ids, sha1s=payload.sha1s, value=payload.value)
99
+ return {'updated_ids': updated, 'value': value}
96
100
 
97
101
 
98
102
  # Route pour supprimer une property d'une image dans la table de jointure entre image et property
99
103
  @app.delete("/image_property")
100
- async def delete_property_route(payload: DeleteImagePropertyPayload) -> DeleteImagePropertyPayload:
101
- await delete_image_property(payload.property_id, payload.sha1)
104
+ async def delete_property_value_route(payload: DeleteImagePropertyPayload) -> DeleteImagePropertyPayload:
105
+ await db.delete_property_value(payload.property_id, image_ids=[payload.image_id])
102
106
  return payload
103
107
 
104
108
 
@@ -109,10 +113,10 @@ async def add_tag(payload: AddTagPayload) -> Tag:
109
113
  return await create_tag(payload.property_id, payload.value, payload.parent_id, payload.color)
110
114
 
111
115
 
112
- @app.get("/tags")
113
- async def get_tags_route(property: Optional[str] = None) -> Tags:
114
- return await get_tags(property)
115
-
116
+ @app.get("/tags", response_class=ORJSONResponse)
117
+ async def get_tags_route(property: Optional[str] = None):
118
+ tags = await get_tags(property)
119
+ return ORJSONResponse(tags)
116
120
 
117
121
  @app.patch("/tags")
118
122
  async def update_tag_route(payload: UpdateTagPayload) -> Tag:
@@ -139,9 +143,18 @@ async def get_folders_route():
139
143
  @app.get('/import_status')
140
144
  async def get_import_status_route():
141
145
  image_import = core.importer
142
- new_image = core.get_new_images()
143
- update = [await get_full_image(i) for i in new_image]
144
- res = {'to_import': image_import.total_import, 'imported': image_import.current_computed, 'new_images': update}
146
+ new_image = image_import.get_new_images()
147
+
148
+ update = []
149
+ if new_image:
150
+ update = await get_full_images(new_image)
151
+
152
+ res = {
153
+ 'to_import': image_import.total_import,
154
+ 'imported': image_import.current_import,
155
+ 'computed': image_import.current_computed,
156
+ 'new_images': update
157
+ }
145
158
  return res
146
159
 
147
160
 
@@ -181,12 +194,41 @@ async def make_clusters_route(payload: MakeClusterPayload) -> list[list[str]]:
181
194
  return await make_clusters(payload.nb_groups, payload.image_list)
182
195
 
183
196
 
184
- @app.get("/similar/")
185
- async def get_similar_images_route(sha1_list: list[str] = Query(None)) -> list:
186
- return await get_similar_images(sha1_list)
197
+ @app.post("/similar")
198
+ async def get_similar_images_route(payload: GetSimilarImagesPayload) -> list:
199
+ return await get_similar_images(payload.sha1_list)
200
+
187
201
 
188
202
  @app.post("/pca")
189
203
  async def start_pca_route():
190
204
  return await compute_all_pca(force=True)
191
205
 
206
+
207
+ @app.post("/project")
208
+ async def change_project_route(payload: ChangeProjectPayload):
209
+ os.environ['PANOPTIC_DATA'] = payload.project
210
+ await db_utils.init()
211
+ return f"changed project to {payload.project}"
212
+
213
+ @app.get('/small/images/{file_path:path}')
214
+ async def get_image(file_path: str):
215
+ path = os.path.join(os.environ['PANOPTIC_DATA'], 'mini', file_path)
216
+ async with aiofiles.open(path, 'rb') as f:
217
+ data = await f.read()
218
+
219
+ ext = path.split('.')[-1]
220
+
221
+ # # media_type here sets the media type of the actual response sent to the client.
222
+ return Response(content=data, media_type="image/" + ext)
223
+
224
+ # app.mount("/small/images/", StaticFiles(directory=os.path.join('PANOPTIC_DATA', 'mini')), name="static")
192
225
  app.mount("/", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "html"), html=True), name="static")
226
+
227
+
228
+ class EndpointFilter(logging.Filter):
229
+ def filter(self, record: logging.LogRecord) -> bool:
230
+ return record.getMessage().find("/import_status") == -1
231
+
232
+
233
+ # Filter out /endpoint
234
+ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
@@ -5,12 +5,12 @@ import numpy as np
5
5
  from sklearn.cluster import DBSCAN, KMeans, estimate_bandwidth, MeanShift
6
6
 
7
7
  from panoptic.compute.utils import load_similarity_tree, SimilarityTreeWithLabel
8
- from panoptic.models import ImageVector
8
+ from panoptic.models import ComputedValue
9
9
 
10
10
  SIMILARITY_TREE: SimilarityTreeWithLabel = load_similarity_tree()
11
11
 
12
12
 
13
- def create_similarity_tree(images: list[ImageVector]):
13
+ def create_similarity_tree(images: list[ComputedValue]):
14
14
  tree = SimilarityTreeWithLabel(images)
15
15
  with open(os.path.join(os.getenv('PANOPTIC_DATA'), 'tree.pkl'), 'wb') as f:
16
16
  pickle.dump(tree, f)
@@ -25,7 +25,7 @@ def get_similar_images(vectors: list[np.ndarray]):
25
25
  return SIMILARITY_TREE.query(vector)
26
26
 
27
27
 
28
- def make_clusters(images: list[ImageVector], *, method='kmeans', **kwargs) -> list[list[str]]:
28
+ def make_clusters(images: list[ComputedValue], *, method='kmeans', **kwargs) -> list[list[str]]:
29
29
  res = []
30
30
  vectors, sha1, ahashs = zip(*[(i.vector, i.sha1, i.ahash) for i in images])
31
31
  sha1 = np.asarray(sha1)
@@ -13,7 +13,7 @@ import glob
13
13
  from PIL import Image
14
14
  from sklearn.decomposition import PCA
15
15
  from sklearn.neighbors import KDTree
16
- from transformers import AutoImageProcessor, MobileNetV2Model, CLIPVisionModel, CLIPProcessor
16
+ from transformers import AutoImageProcessor, MobileNetV2Model
17
17
  # from .ocr import full_ocr
18
18
  from .utils import load_data
19
19
 
@@ -4,10 +4,11 @@ import pickle
4
4
  import numpy as np
5
5
  from sklearn.neighbors import KDTree
6
6
 
7
- from panoptic.models import ImageVector
7
+ from panoptic.models import ComputedValue
8
8
 
9
9
  os.environ['PANOPTIC_DATA'] = os.getenv('PANOPTIC_DATA', os.getcwd())
10
10
 
11
+
11
12
  def load_data(data_path=os.getenv('PANOPTIC_DATA')):
12
13
  if not data_path:
13
14
  raise ValueError("PANOPTIC_DATA was not set")
@@ -42,7 +43,7 @@ def load_similarity_tree():
42
43
 
43
44
 
44
45
  class SimilarityTreeWithLabel:
45
- def __init__(self, images: list[ImageVector]):
46
+ def __init__(self, images: list[ComputedValue]):
46
47
  vectors, sha1_list = zip(*[(i.vector, i.sha1) for i in images])
47
48
  self.image_labels = sha1_list
48
49
  self.tree = KDTree(vectors)
@@ -0,0 +1,266 @@
1
+ import atexit
2
+ import json
3
+ import logging
4
+ import random
5
+ from concurrent.futures import ProcessPoolExecutor
6
+ import time
7
+ from typing import List, Any
8
+
9
+ import pandas
10
+ from fastapi import HTTPException
11
+ from tqdm import tqdm
12
+
13
+ from panoptic.core import db
14
+ from panoptic import compute
15
+ from panoptic.models import PropertyType, JSON, Tag, Property, Tags, Properties, \
16
+ UpdateTagPayload, UpdatePropertyPayload, Image, PropertyValue
17
+ from .image_importer import ImageImporter
18
+ from .image_importer import ImageImporter
19
+
20
+ executor = ProcessPoolExecutor(max_workers=4)
21
+ atexit.register(executor.shutdown)
22
+ importer = ImageImporter(executor)
23
+
24
+
25
+ async def create_property(name: str, property_type: PropertyType, mode='id') -> Property:
26
+ return await db.add_property(name, property_type.value, mode)
27
+
28
+
29
+ async def update_property(payload: UpdatePropertyPayload) -> Property:
30
+ existing_property = await db.get_property_by_id(payload.id)
31
+ if not existing_property:
32
+ raise HTTPException(status_code=400, detail="Trying to modify non existent property")
33
+ new_property = existing_property.copy(update=payload.dict(exclude_unset=True))
34
+ await db.update_property(new_property)
35
+ return new_property
36
+
37
+
38
+ async def get_properties() -> Properties:
39
+ properties = await db.get_properties()
40
+ return {prop.id: prop for prop in properties}
41
+
42
+
43
+ async def delete_property(property_id: str):
44
+ await db.delete_property(property_id)
45
+
46
+
47
+ async def set_property_values(property_id: int, value: Any, image_ids: List[int] = None, sha1s: List[int] = None):
48
+ if image_ids and sha1s:
49
+ raise TypeError('Only image_ids or sha1s should be given as keys. Never both')
50
+
51
+ prop = await db.get_property_by_id(property_id)
52
+ if prop.mode == 'id' and not image_ids:
53
+ raise TypeError(f'Property {property_id}: {prop.name} needs image ids as key [mode: {prop.mode}]')
54
+ if prop.mode == 'sha1' and not sha1s:
55
+ raise TypeError(f'Property {property_id}: {prop.name} needs sha1s as key [mode: {prop.mode}]')
56
+
57
+ if prop.type == PropertyType.tag or prop.type == PropertyType.multi_tags:
58
+ if value and not isinstance(value, list):
59
+ value = [int(value)]
60
+
61
+ return await db.set_property_values(property_id, value, image_ids, sha1s)
62
+
63
+
64
+ async def get_full_images(image_ids: List[int] = None) -> List[Image]:
65
+ images = await db.get_images(image_ids)
66
+ sha1s = list({img.sha1 for img in images})
67
+ property_values = await db.get_property_values(image_ids=image_ids)
68
+ ahashs = await db.get_sha1_ahashs(sha1s=sha1s)
69
+ image_index = {img.id: img for img in images}
70
+
71
+ def assign_value(prop):
72
+ image_index[prop.image_id].properties[prop.property_id] = prop
73
+
74
+ [assign_value(prop) for prop in property_values if prop.image_id >= 0]
75
+
76
+ sha1_properties = {}
77
+
78
+ def register_sha1_value(prop: PropertyValue):
79
+ if prop.sha1 not in sha1_properties:
80
+ sha1_properties[prop.sha1] = []
81
+ sha1_properties[prop.sha1].append(prop)
82
+
83
+ [register_sha1_value(prop) for prop in property_values if prop.image_id < 0]
84
+
85
+ def assign_sha1_value(image_id, prop: PropertyValue):
86
+ image_index[image_id].properties[prop.property_id] = prop
87
+
88
+ [assign_sha1_value(img.id, prop) for img in images if img.sha1 in sha1_properties for prop in
89
+ sha1_properties[img.sha1]]
90
+
91
+ [setattr(img, 'ahash', ahashs[img.sha1]) for img in images if img.sha1 in ahashs]
92
+ return images
93
+
94
+
95
+ async def make_clusters(sensibility: float, sha1s: [str]) -> list[list[str]]:
96
+ """
97
+ Compute clusters and return a list of lists of sha1
98
+ """
99
+ if not sha1s:
100
+ return []
101
+ values = await db.get_sha1_computed_values(sha1s)
102
+ return compute.make_clusters(values, method="kmeans", nb_clusters=sensibility)
103
+
104
+
105
+ async def get_similar_images(sha1s: list[str]):
106
+ vectors = [i.vector for i in await db.get_sha1_computed_values(sha1s)]
107
+ res = compute.get_similar_images(vectors)
108
+ return [img for img in res if img['sha1'] not in sha1s]
109
+
110
+
111
+ async def add_property_to_images(property_id: int, sha1_list: list[str], value: JSON) -> str:
112
+ # first check that the property and the image exist:
113
+ images = await db.get_images(sha1s=sha1_list)
114
+ if await db.get_property_by_id(property_id) and images:
115
+ await db.set_property_values(image_ids=[img.id for img in images], property_id=property_id, value=value)
116
+ # check if a value already exists
117
+ # if await db.get_image_property(sha1, property_id):
118
+ # await db.update_image_property(sha1, property_id, value)
119
+ # else:
120
+ # await db.add_image_property(sha1, property_id, value)
121
+ return value
122
+ else:
123
+ raise HTTPException(status_code=400, detail="Trying to set a value on a non existent property or sha1")
124
+
125
+
126
+ async def read_properties_file(data: pandas.DataFrame):
127
+ filenames, ids, sha1 = zip(*[(i.name, i.id, i.sha1) for i in await db.get_images()])
128
+
129
+ # if there are no duplicates them lets assume the props are sha1
130
+ if data.duplicated('key').sum() == 0:
131
+ prop_mode = "sha1"
132
+ matcher = dict(zip(filenames, sha1))
133
+ else:
134
+ prop_mode = "id"
135
+ matcher = dict(zip(filenames, ids))
136
+ data = data[data.key.isin(filenames)]
137
+ # data = data.drop_duplicates(subset='key', keep='first')
138
+ # add panoptic id or sha1 to the dataframe
139
+ data.loc[data.index, 'panoptic_id'] = data['key'].map(matcher)
140
+
141
+ # first, create new images where ids are the same
142
+ unique_ids = list(data.panoptic_id.unique())
143
+ clones_created = 0
144
+ # TODO: create clones all at once ? it's tricky with the ids to update
145
+ if prop_mode == "id":
146
+ for id in tqdm(unique_ids):
147
+ sub_data = data[data.panoptic_id == id]
148
+ # create clones only if needed
149
+ if len(list(sub_data.panoptic_id)) == 0:
150
+ continue
151
+ image = await db.get_images([id])
152
+ image = image[0]
153
+ new_ids = await db.create_clones(image, sub_data.shape[0] - 1)
154
+ clones_created += len(new_ids)
155
+ data.loc[data.panoptic_id == id, "panoptic_id"] = [image.id, *new_ids]
156
+ logging.getLogger('panoptic').info(f"created {clones_created} new images")
157
+ properties_to_create = data.columns.tolist()
158
+
159
+ # then for each property to create
160
+ for prop in properties_to_create:
161
+ if prop == "key" or prop == "panoptic_id":
162
+ continue
163
+
164
+ # create the property
165
+ prop_name, prop_type = prop.split('[')
166
+ prop_type = PropertyType(prop_type.split(']')[0])
167
+ property = await create_property(prop_name, prop_type, prop_mode)
168
+ # then get all possible values to insert
169
+ prop_values = list(data[prop].unique())
170
+
171
+ # if it's a tag property let's create the tags in the db
172
+ if property.type == PropertyType.tag or property.type == PropertyType.multi_tags:
173
+ tag_matcher = {}
174
+ # TODO: can we optimize to create all tags at once ?
175
+ for value in tqdm(prop_values):
176
+ created_tags = []
177
+ # if it's multi tag, assume tags are separated by a comma and create them separately
178
+ for single_tag in value.split(','):
179
+ colors = ["7c1314", "c31d20", "f94144", "f3722c", "f8961e", "f9c74f", "90be6d", "43aa8b", "577590",
180
+ "9daebe"]
181
+ color = '#' + colors[random.randint(0, len(colors) - 1)]
182
+ tag = await create_tag(property.id, single_tag, 0, color)
183
+ created_tags.append(tag.id)
184
+ tag_matcher[value] = created_tags
185
+ # now change all values in the dataframe with the real value that we are going to insert
186
+ data.loc[data.index, prop] = data[prop].map(tag_matcher)
187
+ await db.set_multiple_property_values(property.id, list(data[prop]), list(data.panoptic_id))
188
+
189
+
190
+ async def add_folder(folder):
191
+ found = await importer.import_folder(folder)
192
+ print(f'found {found} images')
193
+ return folder
194
+
195
+
196
+ new_images = []
197
+
198
+
199
+ def get_new_images():
200
+ copy = [i for i in new_images]
201
+ new_images.clear()
202
+ return copy
203
+
204
+
205
+ async def create_tag(property_id, value, parent_id, color: str) -> Tag:
206
+ existing_tag = await db.get_tag(property_id, value)
207
+ if existing_tag is not None:
208
+ if await db.tag_in_ancestors(existing_tag.id, parent_id):
209
+ raise HTTPException(status_code=400, detail="Adding a tag that is an ancestor of himself")
210
+ existing_tag.parents = list({*existing_tag.parents, parent_id})
211
+ await db.update_tag(existing_tag)
212
+ tag_id = existing_tag.id
213
+ else:
214
+ parents = [parent_id]
215
+ tag_id = await db.add_tag(property_id, value, json.dumps(parents), color)
216
+ return await db.get_tag_by_id(tag_id)
217
+
218
+
219
+ async def update_tag(payload: UpdateTagPayload) -> Tag:
220
+ existing_tag = await db.get_tag_by_id(payload.id)
221
+ if not existing_tag:
222
+ raise HTTPException(status_code=400, detail="Trying to modify non existent tag")
223
+ if await db.tag_in_ancestors(existing_tag.id, payload.parent_id):
224
+ raise HTTPException(status_code=400, detail="Adding a tag that is an ancestor of himself")
225
+ # change only fields of the tags that are set in the payload
226
+ new_tag = existing_tag.copy(update=payload.dict(exclude_unset=True))
227
+ await db.update_tag(new_tag)
228
+ return new_tag
229
+
230
+
231
+ async def delete_tag(tag_id: int) -> List[int]:
232
+ # first delete the tag
233
+ modified_tags = [await db.delete_tag_by_id(tag_id)]
234
+ # when deleting a tag, get all children of this tag
235
+ children = await db.get_tags_by_parent_id(tag_id)
236
+ # and remove parent ref of tag in children
237
+ [modified_tags.extend(await delete_tag_parent(c.id, tag_id)) for c in children]
238
+ # then get all images properties tagged with it
239
+ property_values = await db.get_property_values_with_tag(tag_id)
240
+ # and delete the tag ref from them
241
+ for data in property_values:
242
+ if tag_id in data.value:
243
+ data.value.remove(tag_id)
244
+ await db.set_property_values(data.property_id, data.value, [data.image_id])
245
+ return modified_tags
246
+
247
+
248
+ async def delete_tag_parent(tag_id: int, parent_id: int) -> List[int]:
249
+ print(tag_id, parent_id)
250
+ tag = await db.get_tag_by_id(tag_id)
251
+ tag.parents.remove(parent_id)
252
+ if not tag.parents:
253
+ return await delete_tag(tag.id)
254
+ else:
255
+ await db.update_tag(tag)
256
+ return []
257
+
258
+
259
+ async def get_tags(prop: str = None) -> Tags:
260
+ res = {}
261
+ tag_list = await db.get_tags(prop)
262
+ for tag in tag_list:
263
+ if tag.property_id not in res:
264
+ res[tag.property_id] = {}
265
+ res[tag.property_id][tag.id] = tag
266
+ return res