Skip to content

Commit f435b75

Browse files
committed
Add Object nesting entity tracking fix some bugs with save_changes and update the README.md
1 parent 906f351 commit f435b75

File tree

10 files changed

+120
-30
lines changed

10 files changed

+120
-30
lines changed

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,22 @@ There are three ways to install pyravendb.
4141
foo = session.load("foos/1")
4242
```
4343

44-
load method have the option to track entity for you the only thing you need to do is add ```object_type``` when ou call to load
45-
(load method will return a dynamic_stracture object by default).
44+
load method have the option to track entity for you the only thing you need to do is add ```object_type``` when you call to load
45+
(load method will return a dynamic_stracture object by default) for class with nested object you can call load with ```nested_object_types``` dictionary for the other types. just put in the key the name of the object and in the value his class (without this option you will get the document as it is) .
4646

4747
```
4848
foo = session.load("foos/1", object_type=Foo)
4949
```
50+
51+
```
52+
class FooBar(object):
53+
def __init__(self,name,foo):
54+
self.name = name
55+
self.foo = foo
56+
57+
foo_bar = session.load("FooBars/1", object_type=FooBar,nested_object_types={"foo":Foo})
58+
59+
```
5060
To load several documents at once, supply a list of ids to session.load.
5161

5262
```
@@ -67,10 +77,15 @@ For storing with dict we will use database_commands the put command (see the sou
6777

6878
```
6979
class Foo(object):
70-
def __init__(name,key = None):
71-
self.name = name
72-
self.key = key
80+
def __init__(name,key = None):
81+
self.name = name
82+
self.key = key
7383
84+
class FooBar(object):
85+
def __init__(self,name,foo):
86+
self.name = name
87+
self.foo = foo
88+
7489
with store.open_session() as session:
7590
foo = Foo("PyRavenDB")
7691
session.store(foo)
@@ -87,12 +102,16 @@ with store.open_session() as session:
87102
* ```wait_for_non_stale_results``` - False by default if True the query will wait until the index will be non stale.
88103
* ```includes``` - A list of the properties we like to include in the query.
89104
* ``` with_statistics``` - when True the qury will return stats about the query.
105+
* ```nested_object_types``` - A dict of classes for nested object the key will be the name of the class and the value will be
106+
           the object we want to get for that attribute
90107

91108
```
92109
with store.open_session() as session:
93110
query_result = list(session.query().where_equals("name", "test101")
94111
query_result = list(session.query(object_type=Foo).where_starts_with("name", "n"))
95112
query_result = list(session.query(object_type=Foo).where_ends_with("name", "7"))
113+
query_result = list(session.query(object_type=FooBar,nested_object_types={"foo":Foo}).where(name="foo_bar"))
114+
96115
```
97116

98117
You can also build the query with several options using the builder pattern.

pyravendb/connection/requests_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ def _execute_with_replication(self, path, method, headers, data=None, admin=Fals
106106
if headers is None:
107107
headers = {}
108108
headers.update(self.headers)
109-
response = session.request(method, url=url, json=data, headers=headers)
109+
data = json.dumps(data, default=self.convention.json_default_method)
110+
response = session.request(method, url=url, data=data, headers=headers)
110111
if response.status_code == 412 or response.status_code == 401:
111112
try:
112113
oauth_source = response.headers.__getitem__("OAuth-Source")

pyravendb/data/document_convention.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pyravendb.data.indexes import SortOptions
2+
from datetime import datetime, timedelta
23
from enum import Enum
34
from inflector import Inflector
45

@@ -61,8 +62,20 @@ def __init__(self):
6162
self.timeout = 30
6263
self.failover_behavior = Failover.allow_reads_from_secondaries
6364
self.default_use_optimistic_concurrency = True
65+
self.json_default_method = DocumentConvention.json_default
6466
self._system_database = "system"
6567

68+
@staticmethod
69+
def json_default(o):
70+
if isinstance(o, datetime):
71+
return str(0)
72+
elif isinstance(o, timedelta):
73+
return str(0)
74+
elif getattr(o, "__dict__"):
75+
return o.__dict__
76+
else:
77+
raise TypeError(repr(o) + " is not JSON serializable (Try add a json default method to store convention)")
78+
6679
@staticmethod
6780
def default_transform_plural(name):
6881
return inflector.conditional_plural(2, name)

pyravendb/store/document_session.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,13 @@ def save_entity(self, key, entity, original_metadata, metadata, document, force_
8080
"original_metadata": original_metadata, "etag": metadata.get("etag", None), "key": key,
8181
"force_concurrency_check": force_concurrency_check}
8282

83-
def _convert_and_save_entity(self, key, document, object_type):
83+
def _convert_and_save_entity(self, key, document, object_type, nested_object_types):
8484
if key not in self._entities_by_key:
85-
entity, metadata, original_metadata = Utils.convert_to_entity(document, object_type, self.conventions)
85+
entity, metadata, original_metadata = Utils.convert_to_entity(document, object_type, self.conventions,
86+
nested_object_types)
8687
self.save_entity(key, entity, original_metadata, metadata, document)
8788

88-
def _multi_load(self, keys, object_type, includes):
89+
def _multi_load(self, keys, object_type, includes, nested_object_types):
8990
if len(keys) == 0:
9091
return []
9192

@@ -94,7 +95,7 @@ def _multi_load(self, keys, object_type, includes):
9495
ids_in_includes = [key for key in keys if key in self._includes]
9596
if len(ids_in_includes) > 0:
9697
for include in ids_in_includes:
97-
self._convert_and_save_entity(include, self._includes[include], object_type)
98+
self._convert_and_save_entity(include, self._includes[include], object_type, nested_object_types)
9899
self._includes.pop(include)
99100

100101
ids_of_not_existing_object = [key for key in keys if
@@ -112,19 +113,22 @@ def _multi_load(self, keys, object_type, includes):
112113
if results[i] is None:
113114
self._known_missing_ids.add(ids_of_not_existing_object[i])
114115
continue
115-
self._convert_and_save_entity(keys[i], results[i], object_type)
116+
self._convert_and_save_entity(keys[i], results[i], object_type, nested_object_types)
116117
self.save_includes(response_includes)
117118
return [None if key in self._known_missing_ids else self._entities_by_key[
118119
key] if key in self._entities_by_key else None for key in keys]
119120

120-
def load(self, key_or_keys, object_type=None, includes=None):
121+
def load(self, key_or_keys, object_type=None, includes=None, nested_object_types=None):
121122
"""
122123
@param key_or_keys: Identifier of a document that will be loaded.
123124
:type str or list
124125
@param includes: The path to a reference inside the loaded documents can be list (property name)
125126
:type list or str
126-
@param object_type:the class we want to get
127+
@param object_type: The class we want to get
127128
:type classObj:
129+
@param nested_object_types: A dict of classes for nested object the key will be the name of the class and the
130+
value will be the object we want to get for that attribute
131+
:type str
128132
@return: instance of object_type or None if document with given Id does not exist.
129133
:rtype:object_type or None
130134
"""
@@ -134,15 +138,16 @@ def load(self, key_or_keys, object_type=None, includes=None):
134138
includes = [includes]
135139

136140
if isinstance(key_or_keys, list):
137-
return self._multi_load(key_or_keys, object_type, includes)
141+
return self._multi_load(key_or_keys, object_type, includes, nested_object_types)
138142

139143
if key_or_keys in self._known_missing_ids:
140144
return None
141145
if key_or_keys in self._entities_by_key and not includes:
142146
return self._entities_by_key[key_or_keys]
143147

144148
if key_or_keys in self._includes:
145-
self._convert_and_save_entity(key_or_keys, self._includes[key_or_keys], object_type)
149+
self._convert_and_save_entity(key_or_keys, self._includes[key_or_keys], object_type,
150+
nested_object_types)
146151
self._includes.pop(key_or_keys)
147152
if not includes:
148153
return self._entities_by_key[key_or_keys]
@@ -155,7 +160,7 @@ def load(self, key_or_keys, object_type=None, includes=None):
155160
if len(result) == 0 or result[0] is None:
156161
self._known_missing_ids.add(key_or_keys)
157162
return None
158-
self._convert_and_save_entity(key_or_keys, result[0], object_type)
163+
self._convert_and_save_entity(key_or_keys, result[0], object_type, nested_object_types)
159164
self.save_includes(response_includes)
160165
return self._entities_by_key[key_or_keys] if key_or_keys in self._entities_by_key else None
161166

@@ -310,13 +315,13 @@ def _prepare_for_puts_commands(self, data):
310315
data.entities.append(entity)
311316
if key is not None:
312317
self._entities_by_key.pop(key)
313-
document = entity.__dict__
318+
document = entity.__dict__.copy()
314319
document.pop('Id', None)
315320
data.commands.append(commands_data.PutCommandData(key, etag, document, metadata))
316321

317322
def _has_change(self, entity):
318-
if self._entities_and_metadata[entity]["original_metadata"] != self._entities_and_metadata[entity][
319-
"metadata"] or self._entities_and_metadata[entity]["original_value"] != entity.__dict__:
323+
if self._entities_and_metadata[entity]["original_metadata"] != self._entities_and_metadata[entity]["metadata"] \
324+
or self._entities_and_metadata[entity]["original_value"] != entity.__dict__:
320325
return True
321326
return False
322327

pyravendb/store/session_query.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ def __init__(self, session):
1414
self._sort_fields = set()
1515

1616
def __call__(self, object_type=None, index_name=None, using_default_operator=None,
17-
wait_for_non_stale_results=False, includes=None, with_statistics=False):
17+
wait_for_non_stale_results=False, includes=None, with_statistics=False, nested_object_types=None):
1818
"""
1919
@param index_name: The index name we want to apply
2020
:type index_name: str
2121
@param object_type: The type of the object we want to track the entity too
2222
:type Type
23+
@param nested_object_types: A dict of classes for nested object the key will be the name of the class and the
24+
value will be the object we want to get for that attribute
25+
:type str
2326
@param using_default_operator: If None, by default will use OR operator for the query (can use for OR or AND)
2427
@param with_statistics: Make it True to get the query statistics as well
2528
:type bool
@@ -30,6 +33,7 @@ def __call__(self, object_type=None, index_name=None, using_default_operator=Non
3033
index_name += "/{0}".format(self.session.conventions.default_transform_plural(object_type.__name__))
3134
self.index_name = index_name
3235
self.object_type = object_type
36+
self.nested_object_types = nested_object_types
3337
self.using_default_operator = using_default_operator
3438
self.wait_for_non_stale_results = wait_for_non_stale_results
3539
self.includes = includes
@@ -242,7 +246,8 @@ def _execute_query(self):
242246
response_results = response.pop("Results")
243247
response_includes = response.pop("Includes")
244248
for result in response_results:
245-
entity, metadata, original_metadata = Utils.convert_to_entity(result, self.object_type, conventions)
249+
entity, metadata, original_metadata = Utils.convert_to_entity(result, self.object_type, conventions,
250+
self.nested_object_types)
246251
self.session.save_entity(key=original_metadata["@id"], entity=entity, original_metadata=original_metadata,
247252
metadata=metadata, document=result)
248253
results.append(entity)

pyravendb/tests/session_tests/load_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ def __init__(self, name):
99
self.name = name
1010

1111

12+
class Company(object):
13+
def __init__(self, name, product):
14+
self.name = name
15+
self.product = product
16+
17+
1218
class Foo(object):
1319
def __init__(self, name, key):
1420
self.name = name
@@ -23,6 +29,7 @@ def setUpClass(cls):
2329
cls.db.put("products/10", {"name": "test"}, {})
2430
cls.db.put("orders/105", {"name": "testing_order", "key": 92, "product": "products/101"},
2531
{"Raven-Entity-Name": "Orders"})
32+
cls.db.put("company/1", {"name": "test", "product": {"name": "testing_nested"}}, {})
2633
cls.document_store = documentstore(cls.default_url, cls.default_database)
2734
cls.document_store.initialize()
2835

@@ -50,6 +57,11 @@ def test_load_track_entity_with_object_type(self):
5057
product = session.load("products/101", object_type=Product)
5158
self.assertTrue(isinstance(product, Product))
5259

60+
def test_load_track_entity_with_object_type_and_nested_object(self):
61+
with self.document_store.open_session() as session:
62+
company = session.load("company/1", object_type=Company, nested_object_types={"product": Product})
63+
self.assertTrue(isinstance(company, Company) and isinstance(company.product, Product))
64+
5365
def test_load_track_entity_with_object_type_fail(self):
5466
with self.document_store.open_session() as session:
5567
with self.assertRaises(exceptions.InvalidOperationException):

pyravendb/tests/session_tests/query_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ def __init__(self, name, key):
1111
self.key = key
1212

1313

14+
class Company(object):
15+
def __init__(self, name, product):
16+
self.name = name
17+
self.product = product
18+
19+
1420
class TestQuery(TestBase):
1521
@classmethod
1622
def setUpClass(cls):
@@ -33,6 +39,8 @@ def setUpClass(cls):
3339
{"Raven-Entity-Name": "Products", "Raven-Python-Type": "query_test.Product"})
3440
cls.db.put("orders/105", {"name": "testing_order", "key": 92, "product": "products/108"},
3541
{"Raven-Entity-Name": "Orders"})
42+
cls.db.put("company/1", {"name": "withNesting", "product": {"name": "testing_order", "key": 150}},
43+
{"Raven-Entity-Name": "Companies"})
3644
cls.document_store = documentstore(cls.default_url, cls.default_database)
3745
cls.document_store.initialize()
3846

@@ -124,3 +132,10 @@ def test_where_with_include(self):
124132
list(session.query(wait_for_non_stale_results=True, includes="product").where(key=92))
125133
session.load("products/108")
126134
self.assertEqual(session.number_of_requests_in_session, 1)
135+
136+
def test_query_with_nested_object(self):
137+
with self.document_store.open_session() as session:
138+
query_results = list(
139+
session.query(object_type=Company, nested_object_types={"product": Product}).where_equals(
140+
"name", "withNesting"))
141+
self.assertTrue(isinstance(query_results[0], Company) and isinstance(query_results[0].product, Product))

pyravendb/tests/session_tests/store_entities_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class Foo(object):
88
def __init__(self, name, key):
99
self.name = name
1010
self.key = key
11-
self.Id = None
11+
# self.Id = None
1212

1313

1414
class TestSessionStore(TestBase):

pyravendb/tools/utils.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def is_inherit(parent, child):
8888
return Utils.is_inherit(parent, child.__base__)
8989

9090
@staticmethod
91-
def convert_to_entity(document, object_type, conventions):
91+
def convert_to_entity(document, object_type, conventions, nested_object_types=None):
9292
metadata = document.pop("@metadata")
9393
original_metadata = metadata.copy()
9494
type_from_metadata = conventions.try_get_type_from_metadata(metadata)
@@ -106,8 +106,29 @@ def convert_to_entity(document, object_type, conventions):
106106
"Unable to cast object of type {0} to type {1}".format(object_from_metadata, object_type))
107107
entity.__class__ = object_from_metadata
108108
# Checking the class for initialize
109+
entity_initialize_dict = Utils.make_initialize_dict(document, entity.__class__.__init__)
110+
entity.__init__(**entity_initialize_dict)
111+
if nested_object_types:
112+
for key in nested_object_types:
113+
attr = getattr(entity, key)
114+
if attr:
115+
try:
116+
setattr(entity, key, nested_object_types[key](
117+
**Utils.make_initialize_dict(attr, nested_object_types[key].__init__)))
118+
except TypeError:
119+
pass
120+
121+
if 'Id' in entity.__dict__:
122+
entity.Id = metadata.get('@id', None)
123+
return entity, metadata, original_metadata
124+
125+
@staticmethod
126+
def make_initialize_dict(document, entity_init):
127+
if entity_init is None:
128+
return document
129+
109130
entity_initialize_dict = {}
110-
args, __, __, defaults = inspect.getargspec(entity.__class__.__init__)
131+
args, __, __, defaults = inspect.getargspec(entity_init)
111132
if (len(args) - 1) != len(document):
112133
remainder = len(args)
113134
if defaults:
@@ -118,10 +139,8 @@ def convert_to_entity(document, object_type, conventions):
118139
entity_initialize_dict[args[i]] = document.get(args[i], defaults[i - remainder])
119140
else:
120141
entity_initialize_dict = document
121-
entity.__init__(**entity_initialize_dict)
122-
if 'Id' in entity.__dict__:
123-
entity.Id = metadata.get('@id', None)
124-
return entity, metadata, original_metadata
142+
143+
return entity_initialize_dict
125144

126145
@staticmethod
127146
def to_lucene(value, action):
@@ -171,7 +190,8 @@ def numeric_to_lucene_syntax(value):
171190

172191
if isinstance(value, float) or isinstance(value, int):
173192
value = "Dx{0}".format(value)
174-
if isinstance(value, long):
193+
194+
elif isinstance(value, long):
175195
value = "Lx{0}".format(value)
176196
return value
177197

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
setup(
44
name='pyravendb',
55
packages=find_packages(),
6-
version='1.0.9',
6+
version='1.1.0',
77
description='This is the official python client for RavenDB document database',
88
author='Idan Haim Shalom',
99
author_email='[email protected]',

0 commit comments

Comments
 (0)