polypheny-polynom 0.1.0__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.
- polynom/__init__.py +0 -0
- polynom/application.py +285 -0
- polynom/config.py +89 -0
- polynom/joined_load.py +15 -0
- polynom/model.py +88 -0
- polynom/query.py +247 -0
- polynom/reflection.py +45 -0
- polynom/schema/__init__.py +0 -0
- polynom/schema/cascade.py +19 -0
- polynom/schema/field.py +88 -0
- polynom/schema/migration.py +119 -0
- polynom/schema/polytypes.py +264 -0
- polynom/schema/relationship.py +67 -0
- polynom/schema/schema.py +35 -0
- polynom/schema/schema_registry.py +52 -0
- polynom/session.py +260 -0
- polypheny_polynom-0.1.0.dist-info/METADATA +56 -0
- polypheny_polynom-0.1.0.dist-info/RECORD +30 -0
- polypheny_polynom-0.1.0.dist-info/WHEEL +5 -0
- polypheny_polynom-0.1.0.dist-info/licenses/LICENSE +201 -0
- polypheny_polynom-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/config_test.py +93 -0
- tests/model.py +34 -0
- tests/schema.py +34 -0
- tests/test_model.py +99 -0
- tests/test_polytypes.py +209 -0
- tests/test_query.py +311 -0
- tests/test_relationship.py +57 -0
- tests/test_session.py +423 -0
polynom/__init__.py
ADDED
|
File without changes
|
polynom/application.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import polypheny
|
|
2
|
+
import docker
|
|
3
|
+
import logging
|
|
4
|
+
import socket
|
|
5
|
+
import time
|
|
6
|
+
import polynom.config as cfg
|
|
7
|
+
from polynom.schema.migration import Migrator
|
|
8
|
+
from polynom.session import Session
|
|
9
|
+
from docker.errors import DockerException, NotFound, ImageNotFound
|
|
10
|
+
from polynom.schema.schema_registry import _get_ordered_schemas, _to_json
|
|
11
|
+
from polynom.schema.field import PrimaryKeyField, ForeignKeyField
|
|
12
|
+
from polynom.reflection import SchemaSnapshot, SchemaSnapshotSchema
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class Application:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
app_uuid: str,
|
|
20
|
+
address,
|
|
21
|
+
user: str = cfg.get(cfg.DEFAULT_USER),
|
|
22
|
+
password: str = cfg.get(cfg.DEFAULT_PASS),
|
|
23
|
+
transport: str = cfg.get(cfg.DEFAULT_TRANSPORT),
|
|
24
|
+
use_docker: bool = True,
|
|
25
|
+
migrate: bool = False,
|
|
26
|
+
stop_container: bool = False,
|
|
27
|
+
remove_container: bool = False
|
|
28
|
+
):
|
|
29
|
+
cfg.lock()
|
|
30
|
+
|
|
31
|
+
self._app_uuid = app_uuid
|
|
32
|
+
self._address = address
|
|
33
|
+
self._user = user
|
|
34
|
+
self._password = password
|
|
35
|
+
self._transport = transport
|
|
36
|
+
self._use_docker = use_docker
|
|
37
|
+
self._migrate = migrate
|
|
38
|
+
self._stop_container = stop_container
|
|
39
|
+
self._remove_container = remove_container
|
|
40
|
+
|
|
41
|
+
self._conn = None
|
|
42
|
+
self._cursor = None
|
|
43
|
+
self._initialized = False
|
|
44
|
+
|
|
45
|
+
def __enter__(self):
|
|
46
|
+
if self._initialized:
|
|
47
|
+
raise ValueError("Application must only be initialized once.")
|
|
48
|
+
self._initialized = True
|
|
49
|
+
|
|
50
|
+
if self._use_docker:
|
|
51
|
+
self._deploy_polypheny()
|
|
52
|
+
|
|
53
|
+
self._conn = polypheny.connect(
|
|
54
|
+
self._address,
|
|
55
|
+
username=self._user,
|
|
56
|
+
password=self._password,
|
|
57
|
+
transport=self._transport
|
|
58
|
+
)
|
|
59
|
+
self._cursor = self._conn.cursor()
|
|
60
|
+
|
|
61
|
+
self._verify_schema()
|
|
62
|
+
self._create_schema()
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
66
|
+
if self._cursor:
|
|
67
|
+
self._cursor.close()
|
|
68
|
+
if self._conn:
|
|
69
|
+
self._conn.close()
|
|
70
|
+
|
|
71
|
+
if not self._use_docker:
|
|
72
|
+
return
|
|
73
|
+
if self._stop_container:
|
|
74
|
+
self._stop_container_by_name(cfg.get(cfg.POLYPHENY_CONTAINER_NAME))
|
|
75
|
+
if self._remove_container:
|
|
76
|
+
self._remove_container_by_name(cfg.get(cfg.POLYPHENY_CONTAINER_NAME))
|
|
77
|
+
cfg.unlock()
|
|
78
|
+
|
|
79
|
+
def _wait_for_prism(self, timeout=60):
|
|
80
|
+
start_time = time.time()
|
|
81
|
+
while time.time() - start_time < timeout:
|
|
82
|
+
try:
|
|
83
|
+
conn = polypheny.connect(
|
|
84
|
+
self._address,
|
|
85
|
+
username=self._user,
|
|
86
|
+
password=self._password,
|
|
87
|
+
transport=self._transport
|
|
88
|
+
)
|
|
89
|
+
conn.close()
|
|
90
|
+
return
|
|
91
|
+
except EOFError:
|
|
92
|
+
time.sleep(1)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise RuntimeError(f"Unexpected error while connecting to Polypheny: {e}") from e
|
|
95
|
+
raise TimeoutError("Timed out waiting for Polypheny to become available.")
|
|
96
|
+
|
|
97
|
+
def _deploy_polypheny(self):
|
|
98
|
+
logger.info("Establishing connection to Docker...")
|
|
99
|
+
try:
|
|
100
|
+
client = docker.from_env()
|
|
101
|
+
client.ping()
|
|
102
|
+
except DockerException as e:
|
|
103
|
+
logger.error("Docker is not running or not accessible.")
|
|
104
|
+
raise RuntimeError("Docker is not running or not accessible.") from e
|
|
105
|
+
|
|
106
|
+
container_name = cfg.get(cfg.POLYPHENY_CONTAINER_NAME)
|
|
107
|
+
image_name = cfg.get(cfg.POLYPHENY_IMAGE_NAME)
|
|
108
|
+
ports = cfg.get(cfg.POLYPHENY_PORTS)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
logger.info(f"Checking for presence of Polypheny container '{container_name}'...")
|
|
112
|
+
container = client.containers.get(container_name)
|
|
113
|
+
container.start()
|
|
114
|
+
logger.info(f"Container '{container_name}' found and started.")
|
|
115
|
+
except NotFound:
|
|
116
|
+
logger.info("Polypheny container not found. Deploying a new container. This may take a moment...")
|
|
117
|
+
try:
|
|
118
|
+
client.images.pull(image_name)
|
|
119
|
+
container = client.containers.run(
|
|
120
|
+
image_name,
|
|
121
|
+
name=container_name,
|
|
122
|
+
ports=ports,
|
|
123
|
+
detach=True
|
|
124
|
+
)
|
|
125
|
+
logger.info(f"New Polypheny container '{container_name}' deployed and started.")
|
|
126
|
+
except DockerException as e:
|
|
127
|
+
logger.error(f"Failed to create or run the Polypheny container: {e}")
|
|
128
|
+
raise RuntimeError("Failed to create or run the Polypheny container.") from e
|
|
129
|
+
|
|
130
|
+
logger.info(f"Waiting for Polypheny Prism to become available at {self._address,}...")
|
|
131
|
+
try:
|
|
132
|
+
self._wait_for_prism()
|
|
133
|
+
logger.info("Polypheny Prism is now responsive.")
|
|
134
|
+
except TimeoutError as e:
|
|
135
|
+
logger.error(str(e))
|
|
136
|
+
raise RuntimeError("Polypheny container did not become ready in time.") from e
|
|
137
|
+
|
|
138
|
+
def _verify_schema(self):
|
|
139
|
+
self._process_schema(SchemaSnapshotSchema)
|
|
140
|
+
|
|
141
|
+
session = Session(self)
|
|
142
|
+
with session:
|
|
143
|
+
logger.debug(f"Reading schema snapshot from database for application {self._app_uuid}.")
|
|
144
|
+
previous = SchemaSnapshot.query(session).get(self._app_uuid)
|
|
145
|
+
current_snapshot = _to_json()
|
|
146
|
+
|
|
147
|
+
if not previous:
|
|
148
|
+
logger.debug(f"No schema snapshot found for application {self._app_uuid}. Creating a first one.")
|
|
149
|
+
previous = SchemaSnapshot(current_snapshot, _entry_id=self._app_uuid)
|
|
150
|
+
session.add(previous)
|
|
151
|
+
session.commit()
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
logger.debug(f"Checking for schema changes for application {self._app_uuid}.")
|
|
155
|
+
diff = self._compare_snapshots(previous.snapshot, current_snapshot)
|
|
156
|
+
|
|
157
|
+
if diff and self._migrate:
|
|
158
|
+
logger.debug(f"Schema changes for application {self._app_uuid} found.")
|
|
159
|
+
migrator = Migrator()
|
|
160
|
+
migrator.run(session, diff)
|
|
161
|
+
|
|
162
|
+
previous.snapshot = current_snapshot
|
|
163
|
+
session.commit()
|
|
164
|
+
|
|
165
|
+
def _create_schema(self):
|
|
166
|
+
for schema in _get_ordered_schemas():
|
|
167
|
+
self._process_schema(schema)
|
|
168
|
+
|
|
169
|
+
def _compare_snapshots(self, previous, current):
|
|
170
|
+
diff = {}
|
|
171
|
+
|
|
172
|
+
current_schemas = {s['entity_name']: s for s in current['schemas']}
|
|
173
|
+
previous_schemas = {s['entity_name']: s for s in previous['schemas']}
|
|
174
|
+
|
|
175
|
+
for entity_name, prev_entity in previous_schemas.items():
|
|
176
|
+
curr_entity = current_schemas.get(entity_name)
|
|
177
|
+
|
|
178
|
+
entity_diff = {
|
|
179
|
+
'namespace_name': prev_entity.get('namespace_name'),
|
|
180
|
+
'changes': {}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if not curr_entity:
|
|
184
|
+
diff[entity_name] = entity_diff
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
prev_fields = {f['name']: f for f in prev_entity.get('fields', [])}
|
|
188
|
+
curr_fields = {f['name']: f for f in curr_entity.get('fields', [])}
|
|
189
|
+
handled_prev_fields = set()
|
|
190
|
+
|
|
191
|
+
for curr_name, curr_field in curr_fields.items():
|
|
192
|
+
prev_name = curr_field.get('previous_name')
|
|
193
|
+
if prev_name and prev_name in prev_fields:
|
|
194
|
+
entity_diff['changes'][curr_name] = [prev_fields[prev_name], curr_field]
|
|
195
|
+
handled_prev_fields.add(prev_name)
|
|
196
|
+
continue
|
|
197
|
+
if curr_name not in prev_fields:
|
|
198
|
+
entity_diff['changes'][curr_name] = [None, curr_field]
|
|
199
|
+
|
|
200
|
+
for prev_name, prev_field in prev_fields.items():
|
|
201
|
+
if prev_name in handled_prev_fields:
|
|
202
|
+
continue
|
|
203
|
+
if prev_name not in curr_fields:
|
|
204
|
+
entity_diff['changes'][prev_name] = [prev_field, None]
|
|
205
|
+
elif prev_fields[prev_name] != curr_fields[prev_name]:
|
|
206
|
+
entity_diff['changes'][prev_name] = [prev_field, curr_fields[prev_name]]
|
|
207
|
+
|
|
208
|
+
if entity_diff['changes']:
|
|
209
|
+
diff[entity_name] = entity_diff
|
|
210
|
+
|
|
211
|
+
return diff
|
|
212
|
+
|
|
213
|
+
def _process_schema(self, schema_class):
|
|
214
|
+
entity = schema_class.entity_name
|
|
215
|
+
namespace = schema_class.namespace_name
|
|
216
|
+
fields = schema_class._get_fields()
|
|
217
|
+
|
|
218
|
+
logger.info(f"Initializing entity {entity} in namespace {namespace}.")
|
|
219
|
+
|
|
220
|
+
self._cursor.execute(f'CREATE RELATIONAL NAMESPACE IF NOT EXISTS "{namespace}"')
|
|
221
|
+
logger.debug(f"Created namespace {namespace} if absent.")
|
|
222
|
+
|
|
223
|
+
column_defs = []
|
|
224
|
+
foreign_keys = []
|
|
225
|
+
unique_columns = []
|
|
226
|
+
primary_key_columns = []
|
|
227
|
+
|
|
228
|
+
for field in fields:
|
|
229
|
+
col_def = f'"{field._db_field_name}" {field._polytype._type_string}'
|
|
230
|
+
if not getattr(field, 'nullable', False):
|
|
231
|
+
col_def += " NOT NULL"
|
|
232
|
+
default_value = getattr(field, 'default', None)
|
|
233
|
+
if default_value is not None:
|
|
234
|
+
col_def += f" DEFAULT {field._polytype._to_sql_expression(default_value)}"
|
|
235
|
+
if getattr(field, 'unique', False):
|
|
236
|
+
unique_columns.append(field._db_field_name)
|
|
237
|
+
if isinstance(field, PrimaryKeyField):
|
|
238
|
+
primary_key_columns.append(field._db_field_name)
|
|
239
|
+
column_defs.append(col_def)
|
|
240
|
+
|
|
241
|
+
if isinstance(field, ForeignKeyField):
|
|
242
|
+
fk = (
|
|
243
|
+
f'FOREIGN KEY ("{field._db_field_name}") '
|
|
244
|
+
f'REFERENCES "{field.referenced_entity_name}"("{field.referenced_db_field_name}")'
|
|
245
|
+
)
|
|
246
|
+
foreign_keys.append(fk)
|
|
247
|
+
|
|
248
|
+
constraints = foreign_keys[:]
|
|
249
|
+
if primary_key_columns:
|
|
250
|
+
constraints.append(f"PRIMARY KEY ({', '.join(primary_key_columns)})")
|
|
251
|
+
constraints += [f'UNIQUE ("{col}")' for col in unique_columns]
|
|
252
|
+
|
|
253
|
+
create_stmt = f'CREATE TABLE IF NOT EXISTS "{namespace}"."{entity}" ({", ".join(column_defs + constraints)});'
|
|
254
|
+
self._cursor.executeany('sql', create_stmt, namespace=namespace)
|
|
255
|
+
self._conn.commit()
|
|
256
|
+
|
|
257
|
+
logger.debug(f"Created entity {entity} if absent.")
|
|
258
|
+
|
|
259
|
+
def _stop_container_by_name(self, container_name):
|
|
260
|
+
try:
|
|
261
|
+
client = docker.from_env()
|
|
262
|
+
client.ping()
|
|
263
|
+
container = client.containers.get(container_name)
|
|
264
|
+
if container.status == 'running':
|
|
265
|
+
container.stop()
|
|
266
|
+
logger.info(f"Container '{container_name}' stopped.")
|
|
267
|
+
except NotFound:
|
|
268
|
+
logger.warning(f"No container named '{container_name}' found.")
|
|
269
|
+
except DockerException as e:
|
|
270
|
+
logger.error(f"Failed to stop the container '{container_name}': {e}")
|
|
271
|
+
raise RuntimeError(f"Failed to stop the container '{container_name}'") from e
|
|
272
|
+
|
|
273
|
+
def _remove_container_by_name(self, container_name):
|
|
274
|
+
try:
|
|
275
|
+
client = docker.from_env()
|
|
276
|
+
client.ping()
|
|
277
|
+
container = client.containers.get(container_name)
|
|
278
|
+
container.remove()
|
|
279
|
+
logger.info(f"Container '{container_name}' removed.")
|
|
280
|
+
except NotFound:
|
|
281
|
+
logger.warning(f"No container named '{container_name}' found.")
|
|
282
|
+
except DockerException as e:
|
|
283
|
+
logger.error(f"Failed to remove the container '{container_name}': {e}")
|
|
284
|
+
raise RuntimeError(f"Failed to remove the container '{container_name}'") from e
|
|
285
|
+
|
polynom/config.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# keys
|
|
2
|
+
INTERNAL_NAMESPACE = 'INTERNAL_NAMESPACE'
|
|
3
|
+
CHANGE_LOG_TABLE = 'CHANGE_LOG_TABLE'
|
|
4
|
+
SYSTEM_USER_NAME = 'SYSTEM_USER_NAME'
|
|
5
|
+
SNAPSHOT_TABLE = 'SNAPSHOT_TABLE'
|
|
6
|
+
|
|
7
|
+
DEFAULT_NAMESPACE = 'DEFAULT_NAMESPACE'
|
|
8
|
+
POLYPHENY_CONTAINER_NAME = 'POLYPHENY_CONTAINER_NAME'
|
|
9
|
+
POLYPHENY_IMAGE_NAME = 'POLYPHENY_IMAGE_NAME'
|
|
10
|
+
POLYPHENY_PORTS = 'POLYPHENY_PORTS'
|
|
11
|
+
DEFAULT_TRANSPORT = 'DEFAULT_TRANSPORT'
|
|
12
|
+
DEFAULT_USER = 'DEFAULT_USER'
|
|
13
|
+
DEFAULT_PASS = 'DEFAULT_PASS'
|
|
14
|
+
|
|
15
|
+
CHANGE_LOG_IDENTIFIER = 'CHANGE_LOG_IDENTIFIER'
|
|
16
|
+
|
|
17
|
+
# constants
|
|
18
|
+
_internals = {
|
|
19
|
+
INTERNAL_NAMESPACE: 'internal',
|
|
20
|
+
CHANGE_LOG_TABLE: 'change_log',
|
|
21
|
+
SYSTEM_USER_NAME: 'SYSTEM',
|
|
22
|
+
SNAPSHOT_TABLE: 'snapshot',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# user configurables
|
|
26
|
+
_config = {
|
|
27
|
+
DEFAULT_NAMESPACE: 'public',
|
|
28
|
+
POLYPHENY_CONTAINER_NAME: 'polypheny',
|
|
29
|
+
POLYPHENY_IMAGE_NAME: 'vogti/polypheny',
|
|
30
|
+
POLYPHENY_PORTS: {
|
|
31
|
+
"20590/tcp": 20590,
|
|
32
|
+
"7659/tcp": 7659,
|
|
33
|
+
"80/tcp": 80,
|
|
34
|
+
"8081/tcp": 8081,
|
|
35
|
+
"8082/tcp": 8082,
|
|
36
|
+
},
|
|
37
|
+
DEFAULT_TRANSPORT: 'plain',
|
|
38
|
+
DEFAULT_USER: 'pa',
|
|
39
|
+
DEFAULT_PASS: '',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# derived options
|
|
43
|
+
_derived = {}
|
|
44
|
+
|
|
45
|
+
def _refresh_derived():
|
|
46
|
+
_derived[CHANGE_LOG_IDENTIFIER] = f"{_internals[INTERNAL_NAMESPACE]}.{_internals[CHANGE_LOG_TABLE]}"
|
|
47
|
+
|
|
48
|
+
_refresh_derived()
|
|
49
|
+
|
|
50
|
+
USER_CONFIGURABLE_KEYS = set(_config.keys())
|
|
51
|
+
|
|
52
|
+
_lock_count = 0
|
|
53
|
+
|
|
54
|
+
def lock():
|
|
55
|
+
global _lock_count
|
|
56
|
+
_lock_count += 1
|
|
57
|
+
|
|
58
|
+
def unlock():
|
|
59
|
+
global _lock_count
|
|
60
|
+
if _lock_count == 0:
|
|
61
|
+
raise RuntimeError("Unlock called more times than lock")
|
|
62
|
+
_lock_count -= 1
|
|
63
|
+
|
|
64
|
+
def get(key: str):
|
|
65
|
+
if key in _config:
|
|
66
|
+
return _config[key]
|
|
67
|
+
elif key in _internals:
|
|
68
|
+
return _internals[key]
|
|
69
|
+
elif key in _derived:
|
|
70
|
+
return _derived[key]
|
|
71
|
+
else:
|
|
72
|
+
raise KeyError(f"Unknown config key: {key}")
|
|
73
|
+
|
|
74
|
+
def set(key: str, value):
|
|
75
|
+
if key not in USER_CONFIGURABLE_KEYS:
|
|
76
|
+
raise KeyError(f"Cannot set config key: {key}")
|
|
77
|
+
if _lock_count > 0:
|
|
78
|
+
raise RuntimeError("Configuration is locked. No updates are allowed.")
|
|
79
|
+
_config[key] = value
|
|
80
|
+
_refresh_derived()
|
|
81
|
+
|
|
82
|
+
def set_config(overrides: dict):
|
|
83
|
+
if _lock_count > 0:
|
|
84
|
+
raise RuntimeError("Configuration is locked. No updates are allowed.")
|
|
85
|
+
for key, value in overrides.items():
|
|
86
|
+
set(key, value)
|
|
87
|
+
|
|
88
|
+
def all_config():
|
|
89
|
+
return {**_internals, **_config, **_derived}
|
polynom/joined_load.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, Tuple
|
|
2
|
+
from polynom.query import Query
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Encapsulates content to eagerly load a relationship
|
|
6
|
+
"""
|
|
7
|
+
class JoinedLoad:
|
|
8
|
+
def __init__(self, relationship_attr: Any):
|
|
9
|
+
self.attr = relationship_attr # e.g. Bike.user
|
|
10
|
+
|
|
11
|
+
def apply(self, query: Query):
|
|
12
|
+
related_model = self.attr.get_target_model()
|
|
13
|
+
query.join(related_model)
|
|
14
|
+
alias = f"{related_model.schema.entity_name}_{len(query._joins)}"
|
|
15
|
+
query._eager_loads.append((self.attr._key, related_model, alias))
|
polynom/model.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid as uuidlib
|
|
3
|
+
from typing import Any, Type, TypeVar
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from polynom.query import Query
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class BaseModel:
|
|
11
|
+
schema: Type[Any]
|
|
12
|
+
|
|
13
|
+
def __init__(self, _entry_id: str = None):
|
|
14
|
+
if _entry_id is None:
|
|
15
|
+
_entry_id = str(uuidlib.uuid4())
|
|
16
|
+
self._entry_id = _entry_id
|
|
17
|
+
self._is_active = True
|
|
18
|
+
self._update_snapshot()
|
|
19
|
+
|
|
20
|
+
def __setattr__(self, name, value):
|
|
21
|
+
if name == '_is_active':
|
|
22
|
+
object.__setattr__(self, name, value)
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
if getattr(self, "_is_active", True):
|
|
26
|
+
object.__setattr__(self, name, value)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
self._throw_invalid()
|
|
30
|
+
|
|
31
|
+
def _throw_invalid(self):
|
|
32
|
+
message = f"This instance of entry {self._entry_id} is no longer mapped: it is either outside its session, deleted within its session, or replaced by a query result"
|
|
33
|
+
logger.debug(message)
|
|
34
|
+
raise AttributeError(message)
|
|
35
|
+
|
|
36
|
+
def _update_snapshot(self):
|
|
37
|
+
self._snapshot = {
|
|
38
|
+
field._python_field_name: deepcopy(self.__dict__.get(field._python_field_name))
|
|
39
|
+
for field in self.schema._get_fields()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def _diff(self) -> dict[str, tuple]:
|
|
43
|
+
if not self._is_active:
|
|
44
|
+
self._throw_invalid()
|
|
45
|
+
|
|
46
|
+
changelog = {}
|
|
47
|
+
for field in self.schema._get_fields():
|
|
48
|
+
key = field._python_field_name
|
|
49
|
+
old = self._snapshot.get(key)
|
|
50
|
+
new = self.__dict__.get(key)
|
|
51
|
+
if old != new:
|
|
52
|
+
changelog[key] = (old, new)
|
|
53
|
+
return changelog
|
|
54
|
+
|
|
55
|
+
def __repr__(self):
|
|
56
|
+
field_map = self.schema._get_field_map()
|
|
57
|
+
field_values = {
|
|
58
|
+
name: getattr(self, name, None)
|
|
59
|
+
for name in field_map.keys()
|
|
60
|
+
}
|
|
61
|
+
return f"<{self.__class__.__name__} {field_values}>"
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def query(cls, session):
|
|
65
|
+
return Query(cls, session)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def _from_row(cls: Type[T], row: dict[str, Any]) -> T:
|
|
69
|
+
obj_data = {
|
|
70
|
+
field._python_field_name: field._polytype._from_prism_serializable(row[field._db_field_name])
|
|
71
|
+
for field in cls.schema._get_fields()
|
|
72
|
+
}
|
|
73
|
+
return cls(**obj_data)
|
|
74
|
+
|
|
75
|
+
def _to_update_dict(self) -> dict[str, Any]:
|
|
76
|
+
return {
|
|
77
|
+
field._db_field_name: field._polytype._to_prism_serializable(getattr(self, field._python_field_name))
|
|
78
|
+
for field in self.schema._get_fields()
|
|
79
|
+
if hasattr(self, field._python_field_name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def _to_insert_dict(self) -> dict[str, Any]:
|
|
83
|
+
return {
|
|
84
|
+
field._db_field_name: field._polytype._to_prism_serializable(getattr(self, field._python_field_name))
|
|
85
|
+
for field in self.schema._get_fields()
|
|
86
|
+
if hasattr(self, field._python_field_name)
|
|
87
|
+
}
|
|
88
|
+
|