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.
- autonomous/__init__.py +1 -1
- autonomous/ai/audioagent.py +1 -1
- autonomous/ai/imageagent.py +1 -1
- autonomous/ai/jsonagent.py +1 -1
- autonomous/ai/models/openai.py +81 -53
- autonomous/ai/oaiagent.py +1 -14
- autonomous/ai/textagent.py +1 -1
- autonomous/auth/autoauth.py +10 -10
- autonomous/auth/user.py +17 -2
- autonomous/db/__init__.py +42 -0
- autonomous/db/base/__init__.py +33 -0
- autonomous/db/base/common.py +62 -0
- autonomous/db/base/datastructures.py +476 -0
- autonomous/db/base/document.py +1230 -0
- autonomous/db/base/fields.py +767 -0
- autonomous/db/base/metaclasses.py +468 -0
- autonomous/db/base/utils.py +22 -0
- autonomous/db/common.py +79 -0
- autonomous/db/connection.py +472 -0
- autonomous/db/context_managers.py +313 -0
- autonomous/db/dereference.py +291 -0
- autonomous/db/document.py +1141 -0
- autonomous/db/errors.py +165 -0
- autonomous/db/fields.py +2732 -0
- autonomous/db/mongodb_support.py +24 -0
- autonomous/db/pymongo_support.py +80 -0
- autonomous/db/queryset/__init__.py +28 -0
- autonomous/db/queryset/base.py +2033 -0
- autonomous/db/queryset/field_list.py +88 -0
- autonomous/db/queryset/manager.py +58 -0
- autonomous/db/queryset/queryset.py +189 -0
- autonomous/db/queryset/transform.py +527 -0
- autonomous/db/queryset/visitor.py +189 -0
- autonomous/db/signals.py +59 -0
- autonomous/logger.py +3 -0
- autonomous/model/autoattr.py +56 -41
- autonomous/model/automodel.py +95 -34
- autonomous/storage/imagestorage.py +49 -8
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/METADATA +2 -2
- autonomous_app-0.3.2.dist-info/RECORD +60 -0
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/WHEEL +1 -1
- autonomous_app-0.3.0.dist-info/RECORD +0 -35
- {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/LICENSE +0 -0
- {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)
|
autonomous/db/signals.py
ADDED
|
@@ -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()
|
autonomous/model/autoattr.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
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):
|
autonomous/model/automodel.py
CHANGED
|
@@ -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 .
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
80
|
-
except
|
|
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
|
-
|
|
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.
|
|
153
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|