Skip to content

Commit

Permalink
Merge branch 'release/0.18.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
RKrahl committed Mar 29, 2021
2 parents 4f4d54f + 4183a7e commit 8e6deb7
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 67 deletions.
36 changes: 36 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@ Changelog
=========


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

New features
------------

+ `#76`_, `#78`_: add client side support for searching for multiple
fields introduced in icat.server 4.11.0. Add support for building
the corresponding queries in the in class :class:`icat.query.Query`.

Incompatible changes and deprecations
-------------------------------------

+ Since :class:`icat.query.Query` now also accepts a list of attribute
names rather then only a single one, the corresponding keyword
argument `attribute` has been renamed to `attributes` (in the
plural). Accordingly, the method
:meth:`icat.query.Query.setAttribute` has been renamed to
:meth:`icat.query.Query.setAttributes`. The old names are retained
as aliases, but are deprecated.

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

+ `#79`_: fix an encoding issue in :attr:`icat.client.Client.apiversion`,
only relevant with Python 2.

+ `#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
.. _#79: https://github.com/icatproject/python-icat/pull/79
.. _#80: https://github.com/icatproject/python-icat/pull/80


0.17.0 (2020-04-30)
~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ when it is incompatible with PEP 440.
Copyright and License
---------------------

Copyright 2013–2020
Copyright 2013–2021
Helmholtz-Zentrum Berlin für Materialien und Energie GmbH

Licensed under the `Apache License`_, Version 2.0 (the "License"); you
Expand Down
2 changes: 1 addition & 1 deletion doc/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# -- Project information -----------------------------------------------------

project = 'python-icat'
copyright = ('2013–2020, '
copyright = ('2013–2021, '
'Helmholtz-Zentrum Berlin für Materialien und Energie GmbH')
author = 'Rolf Krahl'

Expand Down
8 changes: 5 additions & 3 deletions doc/src/exception.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ The class hierarchy for the exceptions is::
| +-- IDSNotImplementedError
+-- InternalError
+-- ConfigError
+-- EntityTypeError
+-- TypeError
| +-- EntityTypeError
+-- VersionMethodError
+-- SearchResultError
| +-- SearchAssertionError
Expand All @@ -182,5 +183,6 @@ The class hierarchy for the exceptions is::
+-- DeprecationWarning
+-- ICATDeprecationWarning

Here, :exc:`Exception`, :exc:`Warning`, and :exc:`DeprecationWarning`
are build-in exceptions from the Python standard library.
Here, :exc:`Exception`, :exc:`TypeError`, :exc:`Warning`, and
:exc:`DeprecationWarning` are build-in exceptions from the Python
standard library.
15 changes: 13 additions & 2 deletions doc/src/tutorial-search.rst
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,23 @@ Instead of returning a list of the matching objects, we may also
request single attributes. The result will be a list of the attribute
values of the matching objects. Listing the names of all datasets::

>>> query = Query(client, "Dataset", attribute="name")
>>> query = Query(client, "Dataset", attributes="name")
>>> print(query)
SELECT o.name FROM Dataset o
>>> client.search(query)
[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
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]]
There are also some aggregate functions that may be applied to search
results. Let's count all datasets::

Expand All @@ -395,7 +406,7 @@ average magnetic field applied in the measurements::
... "type.name": "= 'Magnetic field'",
... "type.units": "= 'T'",
... }
>>> query = Query(client, "DatasetParameter", conditions=conditions, attribute="numericValue")
>>> query = Query(client, "DatasetParameter", conditions=conditions, attributes="numericValue")
>>> print(query)
SELECT o.numericValue FROM DatasetParameter o JOIN o.dataset AS ds JOIN ds.investigation AS i JOIN o.type AS t WHERE i.name = '10100601-ST' AND t.name = 'Magnetic field' AND t.units = 'T'
>>> client.search(query)
Expand Down
34 changes: 22 additions & 12 deletions icat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
from icat.entities import getTypeMap
from icat.query import Query
from icat.exception import *
from icat.helper import (simpleqp_unquote, parse_attr_val,
ms_timestamp, disable_logger)
from icat.ids import *
from icat.sslcontext import create_ssl_context, HTTPSTransport
from icat.helper import simpleqp_unquote, parse_attr_val, ms_timestamp

__all__ = ['Client']

Expand Down Expand Up @@ -139,7 +140,7 @@ def __init__(self, url, idsurl=None,
proxy = {}
kwargs['transport'] = HTTPSTransport(self.sslContext, proxy=proxy)
super(Client, self).__init__(self.url, **kwargs)
apiversion = self.getApiVersion()
apiversion = str(self.getApiVersion())
# Translate a version having a trailing '-SNAPSHOT' into
# something that StrictVersion would accept.
apiversion = re.sub(r'-SNAPSHOT$', 'a1', apiversion)
Expand Down Expand Up @@ -208,6 +209,12 @@ def clone(self):
return Class(self.url, **self.kwargs)


def _has_wsdl_type(self, name):
"""Check if this client's WSDL defines a particular type name.
"""
with disable_logger("suds.resolver"):
return self.factory.resolver.find(name)

def new(self, obj, **kwargs):

"""Instantiate a new :class:`icat.entity.Entity` object.
Expand Down Expand Up @@ -280,16 +287,19 @@ def getEntityClass(self, name):
def getEntity(self, obj):
"""Get the corresponding :class:`icat.entity.Entity` for an object.
If obj is a Suds instance object, create a new object with
:meth:`~icat.client.Client.new`. Otherwise do nothing and
return obj unchanged.
if obj is a `fieldSet`, return the list of 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:`icat.entity.Entity` or any type
:rtype: :class:`list` or :class:`icat.entity.Entity` or any type
"""
if isinstance(obj, suds.sudsobject.Object):
if obj.__class__.__name__ == 'fieldSet':
return obj.fields
elif isinstance(obj, suds.sudsobject.Object):
return self.new(obj)
else:
return obj
Expand Down Expand Up @@ -507,10 +517,10 @@ def searchChunked(self, query, skip=0, count=None, chunksize=100):
server. There are a few subtle differences though: the query
must not contain a LIMIT clause (use the skip and count
arguments instead) and should contain an ORDER BY clause. The
return value is an iterator over the items in the search
result rather then a list. The individual search calls are
done lazily, e.g. they are not done until needed to yield the
next item from the iterator.
return value is a generator yielding successively the items in
the search result rather then a list. The individual search
calls are done lazily, e.g. they are not done until needed to
yield the next item from the generator.
.. note::
The result may be defective (omissions, duplicates) if the
Expand Down Expand Up @@ -555,7 +565,7 @@ def searchChunked(self, query, skip=0, count=None, chunksize=100):
call. This is an internal tuning parameter and does not
affect the result.
:type chunksize: :class:`int`
:return: a generator that iterates over the items in the
:return: a generator that successively yields the items in the
search result.
:rtype: generator
"""
Expand Down
8 changes: 6 additions & 2 deletions icat/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,12 @@ def __init__(self, feature, version=None):
% (feature, icatstr))
super(ICATDeprecationWarning, self).__init__(msg)

class EntityTypeError(_BaseException):
"""An invalid entity type has been used."""
class EntityTypeError(_BaseException, TypeError):
"""An invalid entity type has been used.
.. versionchanged:: 0.18.0
Inherit from :exc:`TypeError`.
"""
pass

class VersionMethodError(_BaseException):
Expand Down
13 changes: 13 additions & 0 deletions icat/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"""

import sys
from contextlib import contextmanager
import datetime
import logging
import suds.sax.date


Expand Down Expand Up @@ -219,3 +221,14 @@ def ms_timestamp(dt):
dt = dt.replace(tzinfo=None) - offs
ts = 1000 * (dt - datetime.datetime(1970, 1, 1)).total_seconds()
return int(ts)


@contextmanager
def disable_logger(name):
"""Context manager to temporarily disable a logger.
"""
logger = logging.getLogger(name)
sav_state = logger.disabled
logger.disabled = True
yield
logger.disabled = sav_state
Loading

0 comments on commit 8e6deb7

Please sign in to comment.