Skip to content

Commit

Permalink
Merge branch 'release/0.18.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
RKrahl committed Apr 13, 2021
2 parents 8e6deb7 + af1801d commit c85d5c4
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 39 deletions.
21 changes: 19 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@ Changelog
=========


0.18.1 (2021-04-13)
~~~~~~~~~~~~~~~~~~~

Bug fixes and minor changes
---------------------------

+ `#82`_: Change the search result in the case of multiple fields from
list to tuple.

+ `#76`_, `#81`_: work around an issue in icat.server using `DISTINCT`
in search queries for multiple fields.

.. _#76: https://github.com/icatproject/python-icat/issues/76
.. _#81: https://github.com/icatproject/python-icat/pull/81
.. _#82: https://github.com/icatproject/python-icat/pull/82


0.18.0 (2021-03-29)
~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -32,8 +49,8 @@ Bug fixes and minor changes
+ `#80`_: add :exc:`TypeError` as additional ancestor of
:exc:`icat.exception.EntityTypeError`.

.. _#76: https://github.com/icatproject/python-icat/pull/76
.. _#78: https://github.com/icatproject/python-icat/issues/78
.. _#76: https://github.com/icatproject/python-icat/issues/76
.. _#78: https://github.com/icatproject/python-icat/pull/78
.. _#79: https://github.com/icatproject/python-icat/pull/79
.. _#80: https://github.com/icatproject/python-icat/pull/80

Expand Down
4 changes: 2 additions & 2 deletions doc/src/tutorial-search.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,15 +374,15 @@ values of the matching objects. Listing the names of all datasets::
[e201215, e201216, e208339, e208341, e208342, e208945, e208946, e208947]

As the name of that keyword argument suggests, we may also search for
multiple attributes at once. The result will be a list of attribute
multiple attributes at once. The result will be a tuple of attribute
values rather then a single value for each object found in the query.
This requires an ICAT server version 4.11 or newer though::

>>> query = Query(client, "Dataset", attributes=["investigation.name", "name", "complete", "type.name"])
>>> print(query)
SELECT i.name, o.name, o.complete, t.name FROM Dataset o JOIN o.investigation AS i JOIN o.type AS t
>>> client.search(query)
[[08100122-EF, e201215, False, raw], [08100122-EF, e201216, False, raw], [10100601-ST, e208339, False, raw], [10100601-ST, e208341, False, raw], [10100601-ST, e208342, False, raw], [12100409-ST, e208945, False, raw], [12100409-ST, e208946, False, raw], [12100409-ST, e208947, True, analyzed]]
[(08100122-EF, e201215, False, raw), (08100122-EF, e201216, False, raw), (10100601-ST, e208339, False, raw), (10100601-ST, e208341, False, raw), (10100601-ST, e208342, False, raw), (12100409-ST, e208945, False, raw), (12100409-ST, e208946, False, raw), (12100409-ST, e208947, True, analyzed)]
There are also some aggregate functions that may be applied to search
results. Let's count all datasets::
Expand Down
15 changes: 11 additions & 4 deletions icat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,18 +287,25 @@ def getEntityClass(self, name):
def getEntity(self, obj):
"""Get the corresponding :class:`icat.entity.Entity` for an object.
if obj is a `fieldSet`, return the list of fields. If obj is
any other Suds instance object, create a new entity object
if obj is a `fieldSet`, return a tuple of the fields. If obj
is any other Suds instance object, create a new entity object
with :meth:`~icat.client.Client.new`. Otherwise do nothing
and return obj unchanged.
:param obj: either a Suds instance object or anything.
:type obj: :class:`suds.sudsobject.Object` or any type
:return: the new entity object or obj.
:rtype: :class:`list` or :class:`icat.entity.Entity` or any type
:rtype: :class:`tuple` or :class:`icat.entity.Entity` or any type
.. versionchanged:: 0.18.0
add support of `fieldSet`.
.. versionchanged:: 0.18.1
changed the return type from :class:`list` to
:class:`tuple` in the case of `fieldSet`.
"""
if obj.__class__.__name__ == 'fieldSet':
return obj.fields
return tuple(obj.fields)
elif isinstance(obj, suds.sudsobject.Object):
return self.new(obj)
else:
Expand Down
12 changes: 8 additions & 4 deletions icat/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def setAttributes(self, attributes):
value or a list of attribute values respectively for each
matching entity object. If attributes is :const:`None`,
the result will be the list of matching objects instead.
:type attributes: :class:`str` or :class:`list` of :class:`str`
:type attributes: :class:`str` or iterable of :class:`str`
:raise ValueError: if any name in `attributes` is not valid or
if multiple attributes are provided, but the ICAT server
does not support this.
Expand Down Expand Up @@ -261,7 +261,7 @@ def setOrder(self, order):
any item in the list may also be a tuple of an attribute
name and an order direction, the latter being either "ASC"
or "DESC" for ascending or descending order respectively.
:type order: :class:`list` or :class:`bool`
:type order: iterable or :class:`bool`
:raise ValueError: if `order` contains invalid attributes that
either do not exist or contain one to many relationships.
"""
Expand Down Expand Up @@ -415,8 +415,12 @@ def __str__(self):
else:
res = "o"
if self.aggregate:
for fct in reversed(self.aggregate.split(':')):
res = "%s(%s)" % (fct, res)
if len(self.attributes) > 1 and self.aggregate == "DISTINCT":
# See discussion in #76
res = "%s %s" % (self.aggregate, res)
else:
for fct in reversed(self.aggregate.split(':')):
res = "%s(%s)" % (fct, res)
base = "SELECT %s FROM %s o" % (res, self.entity.BeanName)
joins = ""
for obj in sorted(subst.keys()):
Expand Down
1 change: 1 addition & 0 deletions python-icat.spec
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ rm -rf %{buildroot}
%endif
%doc %{_docdir}/%{name}
%exclude %{_docdir}/%{name}/examples
%exclude %{_docdir}/%{name}/man
%exclude %{_docdir}/%{name}/tutorial

%files examples
Expand Down
26 changes: 13 additions & 13 deletions tests/test_06_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ def test_logout_no_session_error(client):

@pytest.mark.parametrize(("query", "result"), [
pytest.param("SELECT o.name, o.title, o.startDate FROM Investigation o",
[["08100122-EF", "Durol single crystal",
datetime.datetime(2008, 3, 13, 11, 39, 42, tzinfo=cet)],
["10100601-ST", "Ni-Mn-Ga flat cone",
datetime.datetime(2010, 9, 30, 12, 27, 24, tzinfo=cest)],
["12100409-ST", "NiO SC OF1 JUH HHL",
datetime.datetime(2012, 7, 26, 17, 44, 24, tzinfo=cest)]],
[("08100122-EF", "Durol single crystal",
datetime.datetime(2008, 3, 13, 11, 39, 42, tzinfo=cet)),
("10100601-ST", "Ni-Mn-Ga flat cone",
datetime.datetime(2010, 9, 30, 12, 27, 24, tzinfo=cest)),
("12100409-ST", "NiO SC OF1 JUH HHL",
datetime.datetime(2012, 7, 26, 17, 44, 24, tzinfo=cest))],
marks=pytest.mark.skipif("cet is None",
reason="require datetime.timezone")),
("SELECT i.name, ds.name FROM Dataset ds JOIN ds.investigation AS i "
"WHERE i.startDate < '2011-01-01'",
[["08100122-EF", "e201215"],
["08100122-EF", "e201216"],
["10100601-ST", "e208339"],
["10100601-ST", "e208341"],
["10100601-ST", "e208342"]]),
[("08100122-EF", "e201215"),
("08100122-EF", "e201216"),
("10100601-ST", "e208339"),
("10100601-ST", "e208341"),
("10100601-ST", "e208342")]),
])
def test_search_mulitple_fields(client, query, result):
"""Search for mutliple fields.
Expand Down Expand Up @@ -148,8 +148,8 @@ def test_assertedSearch_unique_mulitple_fields(client):
pytest.skip("search for multiple fields not supported by this server")
query = ("SELECT i.name, i.title, i.startDate FROM Investigation i "
"WHERE i.name = '08100122-EF'")
result = ["08100122-EF", "Durol single crystal",
datetime.datetime(2008, 3, 13, 11, 39, 42, tzinfo=cet)]
result = ("08100122-EF", "Durol single crystal",
datetime.datetime(2008, 3, 13, 11, 39, 42, tzinfo=cet))
r = client.assertedSearch(query)[0]
assert isinstance(r, Sequence)
assert r == result
Expand Down
55 changes: 41 additions & 14 deletions tests/test_06_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,14 @@ def test_query_mulitple_attributes(client):
if not client._has_wsdl_type('fieldSet'):
pytest.skip("search for multiple fields not supported by this server")

results = [["08100122-EF", "Durol single crystal",
datetime.datetime(2008, 3, 13, 10, 39, 42, tzinfo=tzinfo)],
["10100601-ST", "Ni-Mn-Ga flat cone",
datetime.datetime(2010, 9, 30, 10, 27, 24, tzinfo=tzinfo)],
["12100409-ST", "NiO SC OF1 JUH HHL",
datetime.datetime(2012, 7, 26, 15, 44, 24, tzinfo=tzinfo)]]
results = [("08100122-EF", "Durol single crystal",
datetime.datetime(2008, 3, 13, 10, 39, 42, tzinfo=tzinfo)),
("10100601-ST", "Ni-Mn-Ga flat cone",
datetime.datetime(2010, 9, 30, 10, 27, 24, tzinfo=tzinfo)),
("12100409-ST", "NiO SC OF1 JUH HHL",
datetime.datetime(2012, 7, 26, 15, 44, 24, tzinfo=tzinfo))]
query = Query(client, "Investigation",
attributes=["name", "title", "startDate"], order=True)
attributes=("name", "title", "startDate"), order=True)
print(str(query))
res = client.search(query)
assert res == results
Expand All @@ -470,13 +470,13 @@ def test_query_mulitple_attributes_related_obj(client):
if not client._has_wsdl_type('fieldSet'):
pytest.skip("search for multiple fields not supported by this server")

results = [["08100122-EF", "e201215"],
["08100122-EF", "e201216"],
["10100601-ST", "e208339"],
["10100601-ST", "e208341"],
["10100601-ST", "e208342"]]
results = [("08100122-EF", "e201215"),
("08100122-EF", "e201216"),
("10100601-ST", "e208339"),
("10100601-ST", "e208341"),
("10100601-ST", "e208342")]
query = Query(client, "Dataset",
attributes=["investigation.name", "name"], order=True,
attributes=("investigation.name", "name"), order=True,
conditions={"investigation.startDate": "< '2011-01-01'"})
print(str(query))
res = client.search(query)
Expand All @@ -490,10 +490,37 @@ def test_query_mulitple_attributes_oldicat_valueerror(client):
pytest.skip("search for multiple fields is supported by this server")

with pytest.raises(ValueError) as err:
query = Query(client, "Investigation", attributes=["name", "title"])
query = Query(client, "Investigation", attributes=("name", "title"))
err_pattern = r"\bICAT server\b.*\bnot support\b.*\bmultiple attributes\b"
assert re.search(err_pattern, str(err.value))

def test_query_mulitple_attributes_distinct(client):
"""Combine DISTINCT with a query for multiple attributes.
This requires a special handling due to some quirks in the
icat.server query poarser. Support for this has been added in
#81.
"""
if not client._has_wsdl_type('fieldSet'):
pytest.skip("search for multiple fields not supported by this server")

# Try the query without DISTINCT first so that we can verify the effect.
query = Query(client, "InvestigationUser",
attributes=("investigation.name", "role"),
conditions={"investigation.name": "= '08100122-EF'"})
print(str(query))
res = client.search(query)
query = Query(client, "InvestigationUser",
attributes=("investigation.name", "role"),
conditions={"investigation.name": "= '08100122-EF'"},
aggregate="DISTINCT")
print(str(query))
res_dist = client.search(query)
# The search with DISTINCT yields less items, but if we eliminate
# duplicates, the result set is the same:
assert len(res) > len(res_dist)
assert set(res) == set(res_dist)

@pytest.mark.skipif(Version(pytest.__version__) < "3.9.0",
reason="pytest.deprecated_call() does not work properly")
def test_query_deprecated_kwarg_attribute(client):
Expand Down

0 comments on commit c85d5c4

Please sign in to comment.