Skip to content

Commit 42caf64

Browse files
committed
Merge branch 'release/0.21.0'
2 parents 9e5451e + 6806a7a commit 42caf64

5 files changed

Lines changed: 323 additions & 54 deletions

File tree

CHANGES.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ Changelog
22
=========
33

44

5+
0.21.0 (2022-01-28)
6+
~~~~~~~~~~~~~~~~~~~
7+
8+
New features
9+
------------
10+
11+
+ `#100`_: Add read only attributes
12+
:attr:`icat.query.Query.select_clause`,
13+
:attr:`icat.query.Query.join_clause`,
14+
:attr:`icat.query.Query.where_clause`,
15+
:attr:`icat.query.Query.order_clause`,
16+
:attr:`icat.query.Query.include_clause`, and
17+
:attr:`icat.query.Query.limit_clause` to access the respective
18+
clauses of the query string.
19+
20+
.. _#100: https://github.com/icatproject/python-icat/pull/100
21+
522
0.20.1 (2021-11-04)
623
~~~~~~~~~~~~~~~~~~~
724

README.rst

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ The latest release version can be found `Python Package Index (PyPI)`__.
2626
System requirements
2727
-------------------
2828

29-
Python:
29+
Python
30+
......
3031

3132
+ 2.7 or 3.3 and newer.
3233

33-
Required Library packages:
34+
Required library packages
35+
.........................
3436

3537
+ Suds. The original version by Jeff Ortel is not maintained anymore
3638
since very long time and not recommended. There are several forks
@@ -41,8 +43,21 @@ Required Library packages:
4143

4244
.. __: `suds-jurko`_
4345

44-
Optional library packages, only needed to use certain extra features,
45-
not required to install or use python-icat itself:
46+
Conflicts with other packages
47+
.............................
48+
49+
+ `setuptools`_ >= 58.0
50+
51+
There is a breaking change in setuptools 58.0 that affects all
52+
python-icat releases older than upcoming 1.0. You must either not
53+
install setuptools at all or at least downgrade it to 57.5 or older
54+
in order to install python-icat.
55+
56+
Optional library packages
57+
.........................
58+
59+
These packages are only needed to use certain extra features. They
60+
are not required to install or use python-icat itself:
4661

4762
+ `PyYAML`_
4863

@@ -230,7 +245,7 @@ when it is incompatible with PEP 440.
230245
Copyright and License
231246
---------------------
232247

233-
Copyright 2013–2021
248+
Copyright 2013–2022
234249
Helmholtz-Zentrum Berlin für Materialien und Energie GmbH
235250

236251
Licensed under the `Apache License`_, Version 2.0 (the "License"); you
@@ -247,6 +262,7 @@ permissions and limitations under the License.
247262
.. _PyPI site: https://pypi.org/project/python-icat/
248263
.. _suds-jurko: https://bitbucket.org/jurko/suds
249264
.. _suds-community: https://github.com/suds-community/suds
265+
.. _setuptools: https://github.com/pypa/setuptools
250266
.. _PyYAML: https://github.com/yaml/pyyaml
251267
.. _lxml: https://lxml.de/
252268
.. _Requests: https://requests.readthedocs.io/

doc/src/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# -- Project information -----------------------------------------------------
1212

1313
project = 'python-icat'
14-
copyright = ('2013–2021, '
14+
copyright = ('2013–2022, '
1515
'Helmholtz-Zentrum Berlin für Materialien und Energie GmbH')
1616
author = 'Rolf Krahl'
1717

icat/query.py

Lines changed: 111 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ def __init__(self, client, entity,
129129
if isinstance(entity, basestring):
130130
self.entity = self.client.getEntityClass(entity)
131131
elif issubclass(entity, icat.entity.Entity):
132-
if (entity in self.client.typemap.values() and
132+
if (entity in self.client.typemap.values() and
133133
entity.BeanName is not None):
134134
self.entity = entity
135135
else:
136-
raise EntityTypeError("Invalid entity type '%s'."
136+
raise EntityTypeError("Invalid entity type '%s'."
137137
% entity.__name__)
138138
else:
139139
raise EntityTypeError("Invalid entity type '%s'." % type(entity))
@@ -155,6 +155,7 @@ def __init__(self, client, entity,
155155
self.setOrder(order)
156156
self.setLimit(limit)
157157
self._init = None
158+
self._subst = None
158159

159160
def _attrpath(self, attrname):
160161
"""Follow the attribute path along related objects and iterate over
@@ -170,12 +171,12 @@ def _attrpath(self, attrname):
170171
if rclass is None:
171172
# Last component was not a relation, no further components
172173
# in the name allowed.
173-
raise ValueError("Invalid attrname '%s' for %s."
174+
raise ValueError("Invalid attrname '%s' for %s."
174175
% (attrname, self.entity.BeanName))
175176
attrInfo = rclass.getAttrInfo(self.client, attr)
176177
if attrInfo.relType == "ATTRIBUTE":
177178
rclass = None
178-
elif (attrInfo.relType == "ONE" or
179+
elif (attrInfo.relType == "ONE" or
179180
attrInfo.relType == "MANY"):
180181
rclass = self.client.getEntityClass(attrInfo.type)
181182
else:
@@ -225,6 +226,14 @@ def _split_db_functs(self, attr):
225226
raise ValueError("Invalid attribute '%s'" % attr)
226227
return m.group(2,1)
227228

229+
def _get_subst(self):
230+
if self._subst is None:
231+
joinattrs = ( set(self.order.keys()) |
232+
set(self.conditions.keys()) |
233+
set(self.attributes) )
234+
self._subst = self._makesubst(joinattrs)
235+
return self._subst
236+
228237
def setAttributes(self, attributes):
229238
"""Set the attributes that the query shall return.
230239
@@ -244,6 +253,7 @@ def setAttributes(self, attributes):
244253
:meth:`setAttribute` to :meth:`setAttributes` (in the
245254
plural).
246255
"""
256+
self._subst = None
247257
self.attributes = []
248258
if attributes:
249259
if isinstance(attributes, str):
@@ -343,6 +353,7 @@ def setOrder(self, order):
343353
.. versionchanged:: 0.20.0
344354
allow a JPQL function in the attribute.
345355
"""
356+
self._subst = None
346357
# Note: with Python 3.7 and newer we could simplify this using
347358
# a standard dict() rather then an OrderedDict().
348359
self.order = OrderedDict()
@@ -359,7 +370,7 @@ def setOrder(self, order):
359370
if isinstance(obj, tuple):
360371
obj, direction = obj
361372
if direction not in ("ASC", "DESC"):
362-
raise ValueError("Invalid ordering direction '%s'"
373+
raise ValueError("Invalid ordering direction '%s'"
363374
% direction)
364375
else:
365376
direction = None
@@ -433,6 +444,7 @@ def _cond_value(rhs, func):
433444
else:
434445
return "%%s %s" % (rhs)
435446
if conditions:
447+
self._subst = None
436448
for k in conditions.keys():
437449
if isinstance(conditions[k], basestring):
438450
conds = [conditions[k]]
@@ -463,7 +475,7 @@ def addIncludes(self, includes):
463475
for (pattr, attrInfo, rclass) in self._attrpath(iobj):
464476
pass
465477
if rclass is None:
466-
raise ValueError("%s.%s is not a related object."
478+
raise ValueError("%s.%s is not a related object."
467479
% (self.entity.BeanName, iobj))
468480
self.includes.update(includes)
469481

@@ -479,36 +491,15 @@ def setLimit(self, limit):
479491
raise TypeError("limit must be a tuple of two elements.")
480492
self.limit = limit
481493
else:
482-
self.limit = None
483-
484-
def __repr__(self):
485-
"""Return a formal representation of the query.
486-
"""
487-
return ("%s(%s, %s, attributes=%s, aggregate=%s, order=%s, "
488-
"conditions=%s, includes=%s, limit=%s, join_specs=%s)"
489-
% (self.__class__.__name__,
490-
repr(self.client), repr(self.entity.BeanName),
491-
repr(self.attributes), repr(self.aggregate),
492-
repr(self.order), repr(self.conditions),
493-
repr(self.includes), repr(self.limit),
494-
repr(self.join_specs)))
494+
self.limit = None
495495

496-
def __str__(self):
497-
"""Return a string representation of the query.
496+
@property
497+
def select_clause(self):
498+
"""The SELECT clause of the query.
498499
499-
Note for Python 2: the result will be an unicode object if any
500-
of the conditions in the query contains unicode. This
501-
violates the specification of the string representation
502-
operator that requires the return value to be a string object.
503-
But it is the *right thing* to do to get queries with
504-
non-ascii characters working. So this operator favours
505-
usefulness over formal correctness. For Python 3, there is no
506-
distinction between Unicode and string objects anyway.
500+
.. versionadded:: 0.21.0
507501
"""
508-
joinattrs = ( set(self.order.keys()) |
509-
set(self.conditions.keys()) |
510-
set(self.attributes) )
511-
subst = self._makesubst(joinattrs)
502+
subst = self._get_subst()
512503
if self.attributes:
513504
attrs = []
514505
for a in self.attributes:
@@ -528,40 +519,115 @@ def __str__(self):
528519
else:
529520
for fct in reversed(self.aggregate.split(':')):
530521
res = "%s(%s)" % (fct, res)
531-
base = "SELECT %s FROM %s o" % (res, self.entity.BeanName)
532-
joins = ""
522+
return "SELECT %s FROM %s o" % (res, self.entity.BeanName)
523+
524+
@property
525+
def join_clause(self):
526+
"""The JOIN clause of the query.
527+
528+
.. versionadded:: 0.21.0
529+
"""
530+
subst = self._get_subst()
531+
joins = []
533532
for obj in sorted(subst.keys()):
534533
js = self.join_specs.get(obj, "JOIN")
535-
joins += " %s %s" % (js, self._dosubst(obj, subst))
534+
joins.append("%s %s" % (js, self._dosubst(obj, subst)))
535+
if joins:
536+
return " ".join(joins)
537+
else:
538+
return None
539+
540+
@property
541+
def where_clause(self):
542+
"""The WHERE clause of the query.
543+
544+
.. versionadded:: 0.21.0
545+
"""
546+
subst = self._get_subst()
536547
if self.conditions:
537548
conds = []
538549
for a in sorted(self.conditions.keys()):
539550
attr = self._dosubst(a, subst, False)
540551
for c in self.conditions[a]:
541552
conds.append(c % attr)
542-
where = " WHERE " + " AND ".join(conds)
553+
return "WHERE " + " AND ".join(conds)
543554
else:
544-
where = ""
555+
return None
556+
557+
@property
558+
def order_clause(self):
559+
"""The ORDER BY clause of the query.
560+
561+
.. versionadded:: 0.21.0
562+
"""
563+
subst = self._get_subst()
545564
if self.order:
546565
orders = []
547566
for a in self.order.keys():
548567
orders.append(self.order[a] % self._dosubst(a, subst, False))
549-
order = " ORDER BY " + ", ".join(orders)
568+
return "ORDER BY " + ", ".join(orders)
550569
else:
551-
order = ""
570+
return None
571+
572+
@property
573+
def include_clause(self):
574+
"""The INCLUDE clause of the query.
575+
576+
.. versionadded:: 0.21.0
577+
"""
552578
if self.includes:
553579
subst = self._makesubst(self.includes)
554580
includes = set(self.includes)
555581
includes.update(subst.keys())
556582
incl = [ self._dosubst(obj, subst) for obj in sorted(includes) ]
557-
include = " INCLUDE " + ", ".join(incl)
583+
return "INCLUDE " + ", ".join(incl)
558584
else:
559-
include = ""
585+
return None
586+
587+
@property
588+
def limit_clause(self):
589+
"""The LIMIT clause of the query.
590+
591+
.. versionadded:: 0.21.0
592+
"""
560593
if self.limit:
561-
limit = " LIMIT %s, %s" % self.limit
594+
return "LIMIT %s, %s" % self.limit
562595
else:
563-
limit = ""
564-
return base + joins + where + order + include + limit
596+
return None
597+
598+
def __repr__(self):
599+
"""Return a formal representation of the query.
600+
"""
601+
return ("%s(%s, %s, attributes=%s, aggregate=%s, order=%s, "
602+
"conditions=%s, includes=%s, limit=%s, join_specs=%s)"
603+
% (self.__class__.__name__,
604+
repr(self.client), repr(self.entity.BeanName),
605+
repr(self.attributes), repr(self.aggregate),
606+
repr(self.order), repr(self.conditions),
607+
repr(self.includes), repr(self.limit),
608+
repr(self.join_specs)))
609+
610+
def __str__(self):
611+
"""Return a string representation of the query.
612+
613+
Note for Python 2: the result will be an unicode object if any
614+
of the conditions in the query contains unicode. This
615+
violates the specification of the string representation
616+
operator that requires the return value to be a string object.
617+
But it is the *right thing* to do to get queries with
618+
non-ascii characters working. So this operator favours
619+
usefulness over formal correctness. For Python 3, there is no
620+
distinction between Unicode and string objects anyway.
621+
"""
622+
clauses = filter(None, (
623+
self.select_clause,
624+
self.join_clause,
625+
self.where_clause,
626+
self.order_clause,
627+
self.include_clause,
628+
self.limit_clause,
629+
))
630+
return " ".join(clauses)
565631

566632
def copy(self):
567633
"""Return an independent clone of this query.

0 commit comments

Comments
 (0)