autonomous-app 0.3.0__py3-none-any.whl → 0.3.2__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 (44) hide show
  1. autonomous/__init__.py +1 -1
  2. autonomous/ai/audioagent.py +1 -1
  3. autonomous/ai/imageagent.py +1 -1
  4. autonomous/ai/jsonagent.py +1 -1
  5. autonomous/ai/models/openai.py +81 -53
  6. autonomous/ai/oaiagent.py +1 -14
  7. autonomous/ai/textagent.py +1 -1
  8. autonomous/auth/autoauth.py +10 -10
  9. autonomous/auth/user.py +17 -2
  10. autonomous/db/__init__.py +42 -0
  11. autonomous/db/base/__init__.py +33 -0
  12. autonomous/db/base/common.py +62 -0
  13. autonomous/db/base/datastructures.py +476 -0
  14. autonomous/db/base/document.py +1230 -0
  15. autonomous/db/base/fields.py +767 -0
  16. autonomous/db/base/metaclasses.py +468 -0
  17. autonomous/db/base/utils.py +22 -0
  18. autonomous/db/common.py +79 -0
  19. autonomous/db/connection.py +472 -0
  20. autonomous/db/context_managers.py +313 -0
  21. autonomous/db/dereference.py +291 -0
  22. autonomous/db/document.py +1141 -0
  23. autonomous/db/errors.py +165 -0
  24. autonomous/db/fields.py +2732 -0
  25. autonomous/db/mongodb_support.py +24 -0
  26. autonomous/db/pymongo_support.py +80 -0
  27. autonomous/db/queryset/__init__.py +28 -0
  28. autonomous/db/queryset/base.py +2033 -0
  29. autonomous/db/queryset/field_list.py +88 -0
  30. autonomous/db/queryset/manager.py +58 -0
  31. autonomous/db/queryset/queryset.py +189 -0
  32. autonomous/db/queryset/transform.py +527 -0
  33. autonomous/db/queryset/visitor.py +189 -0
  34. autonomous/db/signals.py +59 -0
  35. autonomous/logger.py +3 -0
  36. autonomous/model/autoattr.py +56 -41
  37. autonomous/model/automodel.py +95 -34
  38. autonomous/storage/imagestorage.py +49 -8
  39. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/METADATA +2 -2
  40. autonomous_app-0.3.2.dist-info/RECORD +60 -0
  41. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/WHEEL +1 -1
  42. autonomous_app-0.3.0.dist-info/RECORD +0 -35
  43. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/LICENSE +0 -0
  44. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,189 @@
1
+ import copy
2
+ import warnings
3
+
4
+ from autonomous.db.errors import InvalidQueryError
5
+ from autonomous.db.queryset import transform
6
+
7
+ __all__ = ("Q", "QNode")
8
+
9
+
10
+ def warn_empty_is_deprecated():
11
+ msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
12
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
13
+
14
+
15
+ class QNodeVisitor:
16
+ """Base visitor class for visiting Q-object nodes in a query tree."""
17
+
18
+ def visit_combination(self, combination):
19
+ """Called by QCombination objects."""
20
+ return combination
21
+
22
+ def visit_query(self, query):
23
+ """Called by (New)Q objects."""
24
+ return query
25
+
26
+
27
+ class DuplicateQueryConditionsError(InvalidQueryError):
28
+ pass
29
+
30
+
31
+ class SimplificationVisitor(QNodeVisitor):
32
+ """Simplifies query trees by combining unnecessary 'and' connection nodes
33
+ into a single Q-object.
34
+ """
35
+
36
+ def visit_combination(self, combination):
37
+ if combination.operation == combination.AND:
38
+ # The simplification only applies to 'simple' queries
39
+ if all(isinstance(node, Q) for node in combination.children):
40
+ queries = [n.query for n in combination.children]
41
+ try:
42
+ return Q(**self._query_conjunction(queries))
43
+ except DuplicateQueryConditionsError:
44
+ # Cannot be simplified
45
+ pass
46
+ return combination
47
+
48
+ def _query_conjunction(self, queries):
49
+ """Merges query dicts - effectively &ing them together."""
50
+ query_ops = set()
51
+ combined_query = {}
52
+ for query in queries:
53
+ ops = set(query.keys())
54
+ # Make sure that the same operation isn't applied more than once
55
+ # to a single field
56
+ intersection = ops.intersection(query_ops)
57
+ if intersection:
58
+ raise DuplicateQueryConditionsError()
59
+
60
+ query_ops.update(ops)
61
+ combined_query.update(copy.deepcopy(query))
62
+ return combined_query
63
+
64
+
65
+ class QueryCompilerVisitor(QNodeVisitor):
66
+ """Compiles the nodes in a query tree to a PyMongo-compatible query
67
+ dictionary.
68
+ """
69
+
70
+ def __init__(self, document):
71
+ self.document = document
72
+
73
+ def visit_combination(self, combination):
74
+ operator = "$and"
75
+ if combination.operation == combination.OR:
76
+ operator = "$or"
77
+ return {operator: combination.children}
78
+
79
+ def visit_query(self, query):
80
+ return transform.query(self.document, **query.query)
81
+
82
+
83
+ class QNode:
84
+ """Base class for nodes in query trees."""
85
+
86
+ AND = 0
87
+ OR = 1
88
+
89
+ def to_query(self, document):
90
+ query = self.accept(SimplificationVisitor())
91
+ query = query.accept(QueryCompilerVisitor(document))
92
+ return query
93
+
94
+ def accept(self, visitor):
95
+ raise NotImplementedError
96
+
97
+ def _combine(self, other, operation):
98
+ """Combine this node with another node into a QCombination
99
+ object.
100
+ """
101
+ # If the other Q() is empty, ignore it and just use `self`.
102
+ if not bool(other):
103
+ return self
104
+
105
+ # Or if this Q is empty, ignore it and just use `other`.
106
+ if not bool(self):
107
+ return other
108
+
109
+ return QCombination(operation, [self, other])
110
+
111
+ @property
112
+ def empty(self):
113
+ warn_empty_is_deprecated()
114
+ return False
115
+
116
+ def __or__(self, other):
117
+ return self._combine(other, self.OR)
118
+
119
+ def __and__(self, other):
120
+ return self._combine(other, self.AND)
121
+
122
+
123
+ class QCombination(QNode):
124
+ """Represents the combination of several conditions by a given
125
+ logical operator.
126
+ """
127
+
128
+ def __init__(self, operation, children):
129
+ self.operation = operation
130
+ self.children = []
131
+ for node in children:
132
+ # If the child is a combination of the same type, we can merge its
133
+ # children directly into this combinations children
134
+ if isinstance(node, QCombination) and node.operation == operation:
135
+ self.children += node.children
136
+ else:
137
+ self.children.append(node)
138
+
139
+ def __repr__(self):
140
+ op = " & " if self.operation is self.AND else " | "
141
+ return "(%s)" % op.join([repr(node) for node in self.children])
142
+
143
+ def __bool__(self):
144
+ return bool(self.children)
145
+
146
+ def accept(self, visitor):
147
+ for i in range(len(self.children)):
148
+ if isinstance(self.children[i], QNode):
149
+ self.children[i] = self.children[i].accept(visitor)
150
+
151
+ return visitor.visit_combination(self)
152
+
153
+ @property
154
+ def empty(self):
155
+ warn_empty_is_deprecated()
156
+ return not bool(self.children)
157
+
158
+ def __eq__(self, other):
159
+ return (
160
+ self.__class__ == other.__class__
161
+ and self.operation == other.operation
162
+ and self.children == other.children
163
+ )
164
+
165
+
166
+ class Q(QNode):
167
+ """A simple query object, used in a query tree to build up more complex
168
+ query structures.
169
+ """
170
+
171
+ def __init__(self, **query):
172
+ self.query = query
173
+
174
+ def __repr__(self):
175
+ return "Q(**%s)" % repr(self.query)
176
+
177
+ def __bool__(self):
178
+ return bool(self.query)
179
+
180
+ def __eq__(self, other):
181
+ return self.__class__ == other.__class__ and self.query == other.query
182
+
183
+ def accept(self, visitor):
184
+ return visitor.visit_query(self)
185
+
186
+ @property
187
+ def empty(self):
188
+ warn_empty_is_deprecated()
189
+ return not bool(self.query)
@@ -0,0 +1,59 @@
1
+ __all__ = (
2
+ "pre_init",
3
+ "post_init",
4
+ "pre_save",
5
+ "pre_save_post_validation",
6
+ "post_save",
7
+ "pre_delete",
8
+ "post_delete",
9
+ )
10
+
11
+ signals_available = False
12
+ try:
13
+ from blinker import Namespace
14
+
15
+ signals_available = True
16
+ except ImportError:
17
+
18
+ class Namespace:
19
+ def signal(self, name, doc=None):
20
+ return _FakeSignal(name, doc)
21
+
22
+ class _FakeSignal:
23
+ """If blinker is unavailable, create a fake class with the same
24
+ interface that allows sending of signals but will fail with an
25
+ error on anything else. Instead of doing anything on send, it
26
+ will just ignore the arguments and do nothing instead.
27
+ """
28
+
29
+ def __init__(self, name, doc=None):
30
+ self.name = name
31
+ self.__doc__ = doc
32
+
33
+ def _fail(self, *args, **kwargs):
34
+ raise RuntimeError(
35
+ "signalling support is unavailable "
36
+ "because the blinker library is "
37
+ "not installed."
38
+ )
39
+
40
+ send = lambda *a, **kw: None # noqa
41
+ connect = disconnect = has_receivers_for = receivers_for = (
42
+ temporarily_connected_to
43
+ ) = _fail
44
+ del _fail
45
+
46
+
47
+ # the namespace for code signals. If you are not autonomous.db code, do
48
+ # not put signals in here. Create your own namespace instead.
49
+ _signals = Namespace()
50
+
51
+ pre_init = _signals.signal("pre_init")
52
+ post_init = _signals.signal("post_init")
53
+ pre_save = _signals.signal("pre_save")
54
+ pre_save_post_validation = _signals.signal("pre_save_post_validation")
55
+ post_save = _signals.signal("post_save")
56
+ pre_delete = _signals.signal("pre_delete")
57
+ post_delete = _signals.signal("post_delete")
58
+ pre_bulk_insert = _signals.signal("pre_bulk_insert")
59
+ post_bulk_insert = _signals.signal("post_bulk_insert")
autonomous/logger.py CHANGED
@@ -43,6 +43,7 @@ class Logger:
43
43
 
44
44
  def __call__(self, *args, **kwargs):
45
45
  if self.enabled:
46
+ is_printed = kwargs.pop("_print", False)
46
47
  caller = inspect.stack()[1]
47
48
  fn = caller.filename.split("/")[-1]
48
49
  msg = f"\n\n{'='*20}\t{fn}:{caller.function}()::{caller.lineno}\t{'='*20}\n"
@@ -57,6 +58,8 @@ class Logger:
57
58
  current.write(f"{msg}\n")
58
59
  with open(self.logarchive, "a") as archive:
59
60
  archive.write(f"{msg}\n")
61
+ if is_printed:
62
+ print(msg)
60
63
 
61
64
 
62
65
  log = Logger()
@@ -1,7 +1,5 @@
1
- from mongoengine.base import (
2
- get_document,
3
- )
4
- from mongoengine.fields import (
1
+ from autonomous import log
2
+ from autonomous.db.fields import (
5
3
  BooleanField,
6
4
  DateTimeField,
7
5
  DictField,
@@ -10,6 +8,7 @@ from mongoengine.fields import (
10
8
  EnumField,
11
9
  FileField,
12
10
  FloatField,
11
+ GenericLazyReferenceField,
13
12
  GenericReferenceField,
14
13
  ImageField,
15
14
  IntField,
@@ -17,8 +16,6 @@ from mongoengine.fields import (
17
16
  StringField,
18
17
  )
19
18
 
20
- from autonomous import log
21
-
22
19
 
23
20
  class StringAttr(StringField):
24
21
  pass
@@ -55,50 +52,68 @@ class ImageAttr(ImageField):
55
52
  class ReferenceAttr(GenericReferenceField):
56
53
  def __get__(self, instance, owner):
57
54
  try:
58
- # Attempt to retrieve the referenced document
59
- return super().__get__(instance, owner)
60
- except DoesNotExist:
61
- # If the document doesn't exist, return None
55
+ result = super().__get__(instance, owner)
56
+ except DoesNotExist as e:
57
+ log(f"ReferenceAttr Error: {e}")
62
58
  return None
59
+ return result
63
60
 
64
61
 
65
- class ListAttr(ListField):
66
- def clean_references(self, values):
67
- safe_values = []
68
- updated = False
62
+ # class ReferenceAttr(GenericLazyReferenceField):
63
+ # def __get__(self, instance, owner):
64
+ # try:
65
+ # result = super().__get__(instance, owner)
66
+ # except DoesNotExist as e:
67
+ # log(f"ReferenceAttr Error: {e}")
68
+ # return None
69
+ # return result.fetch() if result and result.pk else result
69
70
 
70
- for value in values:
71
- try:
72
- if isinstance(value, dict) and "_cls" in value:
73
- doc_cls = get_document(value["_cls"])
74
- value = doc_cls._get_db().dereference(value["_ref"])
75
- if value:
76
- safe_values.append(value)
77
- else:
78
- updated = True
79
- except DoesNotExist:
80
- updated = True
81
- log("hi")
82
- return safe_values, updated
71
+ # except DoesNotExist:
72
+ # If the document doesn't exist, return None
73
+ # return None
74
+
75
+ # def validate(self, value):
76
+ # if value is not None and not self.required:
77
+ # super().validate(value)
78
+
79
+
80
+ class ListAttr(ListField):
81
+ # pass
82
+ def __get__(self, instance, owner):
83
+ # log(instance, owner)
84
+ results = super().__get__(instance, owner) or []
85
+ # print(self.name, self.field, owner, results)
86
+ if isinstance(self.field, ReferenceAttr):
87
+ i = 0
88
+ while i < len(results):
89
+ try:
90
+ if not results[i]:
91
+ log(f"Removing Object: {results[i]}")
92
+ results.pop(i)
93
+ else:
94
+ i += 1
95
+ except DoesNotExist:
96
+ results.pop(i)
97
+ log(f"Object Not Found: {results[i]}")
98
+ # log(results)
99
+ return results
83
100
 
84
101
 
85
102
  class DictAttr(DictField):
86
- def clean_references(self, values):
87
- safe_values = {}
88
- updated = False
89
- for key, value in values.items():
103
+ def __get__(self, instance, owner):
104
+ # log(instance, owner)
105
+ results = super().__get__(instance, owner) or {}
106
+ # log(self.name, self.field, owner, results)
107
+ for key, lazy_obj in results.items():
90
108
  try:
91
- if isinstance(value, dict) and "_cls" in value:
92
- doc_cls = get_document(value["_cls"])
93
- value = doc_cls._get_db().dereference(value["_ref"])
94
- if value:
95
- safe_values[key] = value
96
- else:
97
- updated = True
109
+ if hasattr(lazy_obj, "fetch"):
110
+ lazy_obj = (
111
+ lazy_obj.fetch() if lazy_obj and lazy_obj.pk else lazy_obj
112
+ )
98
113
  except DoesNotExist:
99
- updated = True
100
-
101
- return safe_values, updated
114
+ log(f"Object Not Found: {lazy_obj}")
115
+ results[key] = lazy_obj
116
+ return results
102
117
 
103
118
 
104
119
  class EnumAttr(EnumField):
@@ -4,47 +4,54 @@ import urllib.parse
4
4
  from datetime import datetime
5
5
 
6
6
  from bson import ObjectId
7
- from mongoengine import Document, connect
8
- from mongoengine.fields import DateTimeField
9
7
 
10
8
  from autonomous import log
11
-
12
- from .autoattr import DictAttr, ListAttr
9
+ from autonomous.db import Document, connect, signals
10
+ from autonomous.db.errors import ValidationError
11
+ from autonomous.db.fields import DateTimeField
12
+ from autonomous.model.autoattr import DictAttr, ListAttr
13
13
 
14
14
  host = os.getenv("DB_HOST", "db")
15
15
  port = os.getenv("DB_PORT", 27017)
16
16
  password = urllib.parse.quote_plus(str(os.getenv("DB_PASSWORD")))
17
17
  username = urllib.parse.quote_plus(str(os.getenv("DB_USERNAME")))
18
- db = os.getenv("DB_DB")
19
- connect(host=f"mongodb://{username}:{password}@{host}:{port}")
18
+ dbname = os.getenv("DB_DB")
19
+ # log(f"Connecting to MongoDB at {host}:{port} with {username}:{password} for {dbname}")
20
+ connect(host=f"mongodb://{username}:{password}@{host}:{port}/{dbname}?authSource=admin")
21
+ # log(f"{db}")
20
22
 
21
23
 
22
24
  class AutoModel(Document):
23
- meta = {"abstract": True, "allow_inheritance": True}
25
+ meta = {"abstract": True, "allow_inheritance": True, "strict": False}
24
26
  last_updated = DateTimeField(default=datetime.now)
25
27
 
26
- def __init__(self, *args, **kwargs):
27
- super().__init__(*args, **kwargs)
28
- if kwargs.pop("pk", None):
29
- self.reload()
30
- for k, v in kwargs.items():
31
- setattr(self, k, v)
32
- self.last_updated = datetime.now()
28
+ def __eq__(self, other):
29
+ return self.pk == other.pk if other else False
33
30
 
34
- for field_name, field in self._fields.items():
35
- value = getattr(self, field_name, None)
36
- # log(
37
- # f"Field: {field_name}, Type:{type(value)}, Value: {value}, {hasattr(field, "clean_references")}"
38
- # )
31
+ @classmethod
32
+ def auto_pre_init(cls, sender, document, **kwargs):
33
+ values = kwargs.pop("values", None)
34
+ if pk := values.get("pk") or values.get("id"):
35
+ # Try to load the existing document from the database
36
+ if existing_doc := sender._get_collection().find_one({"_id": ObjectId(pk)}):
37
+ # Update the current instance with the existing data
38
+ existing_doc.pop("_id", None)
39
+ existing_doc.pop("_cls", None)
40
+ for k, v in existing_doc.items():
41
+ if not values.get(k):
42
+ values[k] = v
39
43
 
40
- if hasattr(field, "clean_references") and value:
41
- cleaned_values, updated = field.clean_references(value)
42
- # log(f"Cleaned Values: {cleaned_values}")
43
- if updated:
44
- setattr(self, field_name, cleaned_values)
44
+ @classmethod
45
+ def _auto_pre_init(cls, sender, document, **kwargs):
46
+ sender.auto_pre_init(sender, document, **kwargs)
45
47
 
46
- def __eq__(self, other):
47
- return self.pk == other.pk if other else False
48
+ @classmethod
49
+ def auto_post_init(cls, sender, document, **kwargs):
50
+ document.last_updated = datetime.now()
51
+
52
+ @classmethod
53
+ def _auto_post_init(cls, sender, document, **kwargs):
54
+ sender.auto_post_init(sender, document, **kwargs)
48
55
 
49
56
  @classmethod
50
57
  def model_name(cls, qualified=False):
@@ -58,7 +65,9 @@ class AutoModel(Document):
58
65
 
59
66
  @classmethod
60
67
  def load_model(cls, model):
61
- module_name, model = model.rsplit(".", 1) if "." in model else ("models", model)
68
+ module_name, model = (
69
+ model.rsplit(".", 1) if "." in model else (f"models.{model.lower()}", model)
70
+ )
62
71
  module = importlib.import_module(module_name)
63
72
  return getattr(module, model)
64
73
 
@@ -73,13 +82,22 @@ class AutoModel(Document):
73
82
  Returns:
74
83
  AutoModel or None: The retrieved AutoModel instance, or None if not found.
75
84
  """
76
- if pk and isinstance(pk, str):
85
+
86
+ if isinstance(pk, str):
77
87
  pk = ObjectId(pk)
88
+ elif isinstance(pk, dict) and "$oid" in pk:
89
+ pk = ObjectId(pk["$oid"])
78
90
  try:
79
- return cls.objects(pk=pk).get()
80
- except Exception as e:
81
- log(e)
91
+ return cls.objects.get(id=pk)
92
+ except cls.DoesNotExist as e:
93
+ log(f"Model {cls.__name__} with pk {pk} not found : {e}")
94
+ return None
95
+ except ValidationError as e:
96
+ log(f"Model Validation failure {cls.__name__} [{pk}]: {e}")
82
97
  return None
98
+ except Exception as e:
99
+ log(f"Error getting model {cls.__name__} with pk {pk}: {e}", _print=True)
100
+ raise e
83
101
 
84
102
  @classmethod
85
103
  def random(cls):
@@ -119,7 +137,14 @@ class AutoModel(Document):
119
137
  Returns:
120
138
  list: A list of AutoModel instances that match the search criteria.
121
139
  """
122
- results = cls.objects(**kwargs)
140
+ new_kwargs = {}
141
+ for k, v in kwargs.items():
142
+ if isinstance(v, str):
143
+ new_k = f"{k}__icontains"
144
+ new_kwargs[new_k] = v
145
+ else:
146
+ new_kwargs[k] = v
147
+ results = cls.objects(**new_kwargs)
123
148
  if _order_by:
124
149
  results = results.order_by(*_order_by)
125
150
  if _limit:
@@ -142,6 +167,20 @@ class AutoModel(Document):
142
167
  """
143
168
  return cls.objects(**kwargs).first()
144
169
 
170
+ @classmethod
171
+ def auto_pre_save(cls, sender, document, **kwargs):
172
+ """
173
+ Post-save hook for this model.
174
+ """
175
+ pass
176
+
177
+ @classmethod
178
+ def _auto_pre_save(cls, sender, document, **kwargs):
179
+ """
180
+ Post-save hook for this model.
181
+ """
182
+ sender.auto_pre_save(sender, document, **kwargs)
183
+
145
184
  def save(self):
146
185
  """
147
186
  Save this model to the database.
@@ -149,11 +188,33 @@ class AutoModel(Document):
149
188
  Returns:
150
189
  int: The primary key (pk) of the saved model.
151
190
  """
152
- # log(self.model_dump_json())
153
- return super().save()
191
+ # log(self.to_json())
192
+ obj = super().save()
193
+ self.pk = obj.pk
194
+ return self.pk
195
+
196
+ @classmethod
197
+ def auto_post_save(cls, sender, document, **kwargs):
198
+ """
199
+ Post-save hook for this model.
200
+ """
201
+ pass
202
+
203
+ @classmethod
204
+ def _auto_post_save(cls, sender, document, **kwargs):
205
+ """
206
+ Post-save hook for this model.
207
+ """
208
+ sender.auto_post_save(sender, document, **kwargs)
154
209
 
155
210
  def delete(self):
156
211
  """
157
212
  Delete this model from the database.
158
213
  """
159
214
  return super().delete()
215
+
216
+
217
+ signals.pre_init.connect(AutoModel._auto_pre_init)
218
+ signals.post_init.connect(AutoModel._auto_post_init)
219
+ signals.pre_save.connect(AutoModel._auto_pre_save)
220
+ signals.post_save.connect(AutoModel._auto_post_save)
@@ -1,3 +1,4 @@
1
+ import glob
1
2
  import io
2
3
  import os
3
4
  import shutil
@@ -14,6 +15,12 @@ class ImageStorage:
14
15
  def __init__(self, path="static/images"):
15
16
  self.base_path = path
16
17
 
18
+ def scan_storage(self, path=None):
19
+ for root, dirs, files in os.walk(path or self.base_path):
20
+ for file in files:
21
+ if file == "orig.webp":
22
+ yield os.path.join(root, file)
23
+
17
24
  @classmethod
18
25
  def _get_key(cls, folder="", pkey=None):
19
26
  if folder and not folder.endswith("/"):
@@ -68,10 +75,10 @@ class ImageStorage:
68
75
  if not asset_id:
69
76
  return ""
70
77
  original_path = f"{self.get_path(asset_id)}"
71
- # log(f"Getting image: {asset_id}.{size}", original_path)
78
+ # log(f"Getting image: {asset_id}", original_path)
72
79
  if not os.path.exists(original_path):
73
80
  log(f"Original image not found: {original_path}")
74
- return ""
81
+ return None
75
82
  file_path = f"{original_path}/{size}.webp"
76
83
  # log(file_path)
77
84
  result_url = f"/{file_path}"
@@ -104,10 +111,7 @@ class ImageStorage:
104
111
 
105
112
  def get_path(self, asset_id):
106
113
  if asset_id:
107
- asset_path = asset_id.replace(".", "/")
108
- if asset_path.endswith("/"):
109
- asset_path = asset_path[:-1]
110
- return os.path.join(self.base_path, f"{asset_path}")
114
+ return os.path.join(self.base_path, f"{asset_id}")
111
115
  else:
112
116
  return self.base_path
113
117
 
@@ -124,8 +128,45 @@ class ImageStorage:
124
128
  return imgs
125
129
 
126
130
  def remove(self, asset_id):
131
+ if not asset_id:
132
+ return False
133
+ file_path = self.get_path(asset_id)
134
+ if os.path.isdir(file_path):
135
+ print(f"Removing {file_path}")
136
+ # return shutil.rmtree(file_path, ignore_errors=True)
137
+ return False
138
+
139
+ def clear_cached(self, asset_id):
127
140
  file_path = self.get_path(asset_id)
128
141
  if os.path.isdir(file_path):
129
- shutil.rmtree(file_path)
130
- return True
142
+ for file in glob.glob(os.path.join(file_path, "*")):
143
+ if os.path.basename(file) != "orig.webp":
144
+ os.remove(file)
145
+ return False
146
+
147
+ def rotate(self, asset_id, amount=-90):
148
+ file_path = self.get_path(asset_id)
149
+ log(asset_id)
150
+ with Image.open(f"{file_path}/orig.webp") as img:
151
+ # Rotate the image 90 degrees
152
+ rotated_img = img.rotate(amount, expand=True)
153
+ # Save the rotated image
154
+ log(img, rotated_img)
155
+ # img = img.copy()
156
+ # img_byte_arr = io.BytesIO()
157
+ # img.save(img_byte_arr, )
158
+ self.clear_cached(asset_id)
159
+ rotated_img.save(f"{file_path}/orig.webp", format="WEBP")
160
+ return False
161
+
162
+ def flip(self, asset_id, flipx=True, flipy=True):
163
+ file_path = self.get_path(asset_id)
164
+ with Image.open(f"{file_path}/orig.webp") as img:
165
+ if flipx:
166
+ rotated_img = img.transpose(Image.FLIP_LEFT_RIGHT)
167
+ if flipy:
168
+ rotated_img = img.transpose(Image.FLIP_TOP_BOTTOM)
169
+ # Save the rotated image
170
+ rotated_img.save(f"{file_path}/orig.webp", format="WEBP")
171
+ self.clear_cached(asset_id)
131
172
  return False