Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit df1fc72

Browse files
bartonipfilak-sap
authored andcommittedJun 29, 2020
Implemented Django style filtering
Closes #113 Signed-off-by: Jakub Filak <[email protected]>
1 parent 2d7cd16 commit df1fc72

File tree

5 files changed

+642
-6
lines changed

5 files changed

+642
-6
lines changed
 

‎docs/usage/querying.rst‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,36 @@ Print unique identification (Id) of all employees with name John Smith:
6666
print(smith.EmployeeID)
6767
6868
69+
Get entities matching a filter in ORM style
70+
---------------------------------------------------
71+
72+
Print unique identification (Id) of all employees with name John Smith:
73+
74+
.. code-block:: python
75+
76+
from pyodata.v2.service import GetEntitySetFilter as esf
77+
78+
smith_employees_request = northwind.entity_sets.Employees.get_entities()
79+
smith_employees_request = smith_employees_request.filter(FirstName="John", LastName="Smith")
80+
for smith in smith_employees_request.execute():
81+
print(smith.EmployeeID)
82+
83+
84+
Get entities matching a complex filter in ORM style
85+
---------------------------------------------------
86+
87+
Print unique identification (Id) of all employees with name John Smith:
88+
89+
.. code-block:: python
90+
91+
from pyodata.v2.service import GetEntitySetFilter as esf
92+
93+
smith_employees_request = northwind.entity_sets.Employees.get_entities()
94+
smith_employees_request = smith_employees_request.filter(FirstName__contains="oh", LastName__startswith="Smi")
95+
for smith in smith_employees_request.execute():
96+
print(smith.EmployeeID)
97+
98+
6999
Get a count of entities
70100
-----------------------
71101

‎pyodata/v2/model.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,9 @@ def proprty(self, property_name):
12871287
def proprties(self):
12881288
return list(self._properties.values())
12891289

1290+
def has_proprty(self, proprty_name):
1291+
return proprty_name in self._properties
1292+
12901293
@classmethod
12911294
def from_etree(cls, type_node, config: Config):
12921295
name = type_node.get('Name')

‎pyodata/v2/service.py‎

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,9 @@ def execute(self):
314314
if body:
315315
self._logger.debug(' body: %s', body)
316316

317+
params = "&".join("%s=%s" % (k, v) for k, v in self.get_query_params().items())
317318
response = self._connection.request(
318-
self.get_method(), url, headers=headers, params=self.get_query_params(), data=body)
319+
self.get_method(), url, headers=headers, params=params, data=body)
319320

320321
self._logger.debug('Received response')
321322
self._logger.debug(' url: %s', response.url)
@@ -623,7 +624,7 @@ def expand(self, expand):
623624
def filter(self, filter_val):
624625
"""Sets the filter expression."""
625626
# returns QueryRequest
626-
self._filter = quote(filter_val)
627+
self._filter = filter_val
627628
return self
628629

629630
# def nav(self, key_value, nav_property):
@@ -993,6 +994,212 @@ def __gt__(self, value):
993994
return GetEntitySetFilter.format_filter(self._proprty, 'gt', value)
994995

995996

997+
class FilterExpression:
998+
"""A class representing named expression of OData $filter"""
999+
1000+
def __init__(self, **kwargs):
1001+
self._expressions = kwargs
1002+
self._other = None
1003+
self._operator = None
1004+
1005+
@property
1006+
def expressions(self):
1007+
"""Get expressions where key is property name with the operator suffix
1008+
and value is the left hand side operand.
1009+
"""
1010+
1011+
return self._expressions.items()
1012+
1013+
@property
1014+
def other(self):
1015+
"""Get an instance of the other operand"""
1016+
1017+
return self._other
1018+
1019+
@property
1020+
def operator(self):
1021+
"""The other operand"""
1022+
1023+
return self._operator
1024+
1025+
def __or__(self, other):
1026+
if self._other is not None:
1027+
raise RuntimeError('The FilterExpression already initialized')
1028+
1029+
self._other = other
1030+
self._operator = "or"
1031+
return self
1032+
1033+
def __and__(self, other):
1034+
if self._other is not None:
1035+
raise RuntimeError('The FilterExpression already initialized')
1036+
1037+
self._other = other
1038+
self._operator = "and"
1039+
return self
1040+
1041+
1042+
class GetEntitySetFilterChainable:
1043+
"""
1044+
Example expressions
1045+
FirstName='Tim'
1046+
FirstName__contains='Tim'
1047+
Age__gt=56
1048+
Age__gte=6
1049+
Age__lt=78
1050+
Age__lte=90
1051+
Age__range=(5,9)
1052+
FirstName__in=['Tim', 'Bob', 'Sam']
1053+
FirstName__startswith='Tim'
1054+
FirstName__endswith='mothy'
1055+
Addresses__Suburb='Chatswood'
1056+
Addresses__Suburb__contains='wood'
1057+
"""
1058+
1059+
OPERATORS = [
1060+
'startswith',
1061+
'endswith',
1062+
'lt',
1063+
'lte',
1064+
'gt',
1065+
'gte',
1066+
'contains',
1067+
'range',
1068+
'in',
1069+
'length',
1070+
'eq'
1071+
]
1072+
1073+
def __init__(self, entity_type, filter_expressions, exprs):
1074+
self._entity_type = entity_type
1075+
self._filter_expressions = filter_expressions
1076+
self._expressions = exprs
1077+
1078+
@property
1079+
def expressions(self):
1080+
"""Get expressions as a list of tuples where the first item
1081+
is a property name with the operator suffix and the second item
1082+
is a left hand side value.
1083+
"""
1084+
1085+
return self._expressions.items()
1086+
1087+
def proprty_obj(self, name):
1088+
"""Returns a model property for a particular property"""
1089+
1090+
return self._entity_type.proprty(name)
1091+
1092+
def _decode_and_combine_filter_expression(self, filter_expression):
1093+
filter_expressions = [self._decode_expression(expr, val) for expr, val in filter_expression.expressions]
1094+
return self._combine_expressions(filter_expressions)
1095+
1096+
def _process_query_objects(self):
1097+
"""Processes FilterExpression objects to OData lookups"""
1098+
1099+
filter_expressions = []
1100+
1101+
for expr in self._filter_expressions:
1102+
lhs_expressions = self._decode_and_combine_filter_expression(expr)
1103+
1104+
if expr.other is not None:
1105+
rhs_expressions = self._decode_and_combine_filter_expression(expr.other)
1106+
filter_expressions.append(f'({lhs_expressions}) {expr.operator} ({rhs_expressions})')
1107+
else:
1108+
filter_expressions.append(lhs_expressions)
1109+
1110+
return filter_expressions
1111+
1112+
def _process_expressions(self):
1113+
filter_expressions = [self._decode_expression(expr, val) for expr, val in self.expressions]
1114+
1115+
filter_expressions.extend(self._process_query_objects())
1116+
1117+
return filter_expressions
1118+
1119+
def _decode_expression(self, expr, val):
1120+
field = None
1121+
# field_heirarchy = []
1122+
operator = 'eq'
1123+
exprs = expr.split('__')
1124+
1125+
for part in exprs:
1126+
if self._entity_type.has_proprty(part):
1127+
field = part
1128+
# field_heirarchy.append(part)
1129+
elif part in self.__class__.OPERATORS:
1130+
operator = part
1131+
else:
1132+
raise ValueError(f'"{part}" is not a valid property or operator')
1133+
# field = '/'.join(field_heirarchy)
1134+
1135+
# target_field = self.proprty_obj(field_heirarchy[-1])
1136+
expression = self._build_expression(field, operator, val)
1137+
1138+
return expression
1139+
1140+
# pylint: disable=no-self-use
1141+
def _combine_expressions(self, expressions):
1142+
return ' and '.join(expressions)
1143+
1144+
# pylint: disable=too-many-return-statements, too-many-branches
1145+
def _build_expression(self, field_name, operator, value):
1146+
target_field = self.proprty_obj(field_name)
1147+
1148+
if operator not in ['length', 'in', 'range']:
1149+
value = target_field.to_literal(value)
1150+
1151+
if operator == 'lt':
1152+
return f'{field_name} lt {value}'
1153+
1154+
if operator == 'lte':
1155+
return f'{field_name} le {value}'
1156+
1157+
if operator == 'gte':
1158+
return f'{field_name} ge {value}'
1159+
1160+
if operator == 'gt':
1161+
return f'{field_name} gt {value}'
1162+
1163+
if operator == 'startswith':
1164+
return f'startswith({field_name}, {value}) eq true'
1165+
1166+
if operator == 'endswith':
1167+
return f'endswith({field_name}, {value}) eq true'
1168+
1169+
if operator == 'length':
1170+
value = int(value)
1171+
return f'length({field_name}) eq {value}'
1172+
1173+
if operator in ['contains']:
1174+
return f'substringof({value}, {field_name}) eq true'
1175+
1176+
if operator == 'range':
1177+
if not isinstance(value, (tuple, list)):
1178+
raise TypeError('Range must be tuple or list not {}'.format(type(value)))
1179+
1180+
if len(value) != 2:
1181+
raise ValueError('Only two items can be passed in a range.')
1182+
1183+
low_bound = target_field.to_literal(value[0])
1184+
high_bound = target_field.to_literal(value[1])
1185+
1186+
return f'{field_name} gte {low_bound} and {field_name} lte {high_bound}'
1187+
1188+
if operator == 'in':
1189+
literal_values = (f'{field_name} eq {target_field.to_literal(item)}' for item in value)
1190+
return ' or '.join(literal_values)
1191+
1192+
if operator == 'eq':
1193+
return f'{field_name} eq {value}'
1194+
1195+
raise ValueError(f'Invalid expression {operator}')
1196+
1197+
def __str__(self):
1198+
expressions = self._process_expressions()
1199+
result = self._combine_expressions(expressions)
1200+
return quote(result)
1201+
1202+
9961203
class GetEntitySetRequest(QueryRequest):
9971204
"""GET on EntitySet"""
9981205

@@ -1005,6 +1212,19 @@ def __getattr__(self, name):
10051212
proprty = self._entity_type.proprty(name)
10061213
return GetEntitySetFilter(proprty)
10071214

1215+
def _set_filter(self, filter_val):
1216+
filter_text = self._filter + ' and ' if self._filter else ''
1217+
filter_text += filter_val
1218+
self._filter = filter_text
1219+
1220+
def filter(self, *args, **kwargs):
1221+
if args and len(args) == 1 and isinstance(args[0], str):
1222+
self._filter = args[0]
1223+
else:
1224+
self._set_filter(str(GetEntitySetFilterChainable(self._entity_type, args, kwargs)))
1225+
1226+
return self
1227+
10081228

10091229
class EntitySetProxy:
10101230
"""EntitySet Proxy"""

‎tests/test_model_v2.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77
from pyodata.v2.model import Schema, Typ, StructTypeProperty, Types, EntityType, EdmStructTypeSerializer, \
88
Association, AssociationSet, EndRole, AssociationSetEndRole, TypeInfo, MetadataBuilder, ParserError, PolicyWarning, \
9-
PolicyIgnore, Config, PolicyFatal, NullType, NullAssociation, current_timezone
9+
PolicyIgnore, Config, PolicyFatal, NullType, NullAssociation, current_timezone, StructType
1010
from pyodata.exceptions import PyODataException, PyODataModelError, PyODataParserError
1111
from tests.conftest import assert_logging_policy
1212

@@ -1404,3 +1404,23 @@ def test_missing_property_referenced_in_annotation(mock_warning, xml_builder_fac
14041404
)).build()
14051405

14061406
assert mock_warning.called is False
1407+
1408+
1409+
def test_struct_type_has_property_initial_instance():
1410+
struct_type = StructType('Name', 'Label', False)
1411+
1412+
assert struct_type.has_proprty('proprty') == False
1413+
1414+
1415+
def test_struct_type_has_property_no():
1416+
struct_type = StructType('Name', 'Label', False)
1417+
struct_type._properties['foo'] = 'ugly test hack'
1418+
1419+
assert not struct_type.has_proprty('proprty')
1420+
1421+
1422+
def test_struct_type_has_property_yes():
1423+
struct_type = StructType('Name', 'Label', False)
1424+
struct_type._properties['proprty'] = 'ugly test hack'
1425+
1426+
assert struct_type.has_proprty('proprty')

‎tests/test_service_v2.py‎

Lines changed: 366 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,14 +1640,14 @@ def test_navigation_count(service):
16401640

16411641

16421642
@responses.activate
1643-
def test_navigation_count_with_filter(service):
1644-
"""Check getting $count via navigation property with $filter"""
1643+
def test_count_with_filter(service):
1644+
"""Check getting $count with $filter"""
16451645

16461646
# pylint: disable=redefined-outer-name
16471647

16481648
responses.add(
16491649
responses.GET,
1650-
"{0}/Employees(23)/Addresses/$count?%24filter=City%2520eq%2520%2527London%2527".format(service.url),
1650+
"{0}/Employees(23)/Addresses/$count?%24filter=City%20eq%20%27London%27".format(service.url),
16511651
json=3,
16521652
status=200)
16531653

@@ -1659,6 +1659,369 @@ def test_navigation_count_with_filter(service):
16591659
assert request.execute() == 3
16601660

16611661

1662+
@responses.activate
1663+
def test_count_with_chainable_filter(service):
1664+
"""Check getting $count with $filter and using new filter syntax"""
1665+
1666+
# pylint: disable=redefined-outer-name
1667+
1668+
responses.add(
1669+
responses.GET,
1670+
"{0}/Employees(23)/Addresses/$count?%24filter=City%20eq%20%27London%27".format(service.url),
1671+
json=3,
1672+
status=200)
1673+
1674+
employees = service.entity_sets.Employees.get_entity(23).nav('Addresses').get_entities()
1675+
request = employees.filter(City="London").count()
1676+
1677+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1678+
1679+
assert request.execute() == 3
1680+
1681+
1682+
@responses.activate
1683+
def test_count_with_chainable_filter_lt_operator(service):
1684+
"""Check getting $count with $filter with new filter syntax using multiple filters"""
1685+
1686+
# pylint: disable=redefined-outer-name
1687+
1688+
responses.add(
1689+
responses.GET,
1690+
"{0}/Employees/$count?%24filter=ID%20lt%2023".format(service.url),
1691+
json=3,
1692+
status=200)
1693+
1694+
employees = service.entity_sets.Employees.get_entities()
1695+
request = employees.filter(ID__lt=23).count()
1696+
1697+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1698+
1699+
assert request.execute() == 3
1700+
1701+
1702+
@responses.activate
1703+
def test_count_with_chainable_filter_lte_operator(service):
1704+
"""Check getting $count with $filter with new filter syntax using multiple filters"""
1705+
1706+
# pylint: disable=redefined-outer-name
1707+
1708+
responses.add(
1709+
responses.GET,
1710+
"{0}/Employees/$count?%24filter=ID%20le%2023".format(service.url),
1711+
json=3,
1712+
status=200)
1713+
1714+
employees = service.entity_sets.Employees.get_entities()
1715+
request = employees.filter(ID__lte=23).count()
1716+
1717+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1718+
1719+
assert request.execute() == 3
1720+
1721+
1722+
@responses.activate
1723+
def test_count_with_chainable_filter_gt_operator(service):
1724+
"""Check getting $count with $filter with new filter syntax using multiple filters"""
1725+
1726+
# pylint: disable=redefined-outer-name
1727+
1728+
responses.add(
1729+
responses.GET,
1730+
"{0}/Employees/$count?%24filter=ID%20gt%2023".format(service.url),
1731+
json=3,
1732+
status=200)
1733+
1734+
employees = service.entity_sets.Employees.get_entities()
1735+
request = employees.filter(ID__gt=23).count()
1736+
1737+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1738+
1739+
assert request.execute() == 3
1740+
1741+
1742+
@responses.activate
1743+
def test_count_with_chainable_filter_gte_operator(service):
1744+
"""Check getting $count with $filter with new filter syntax using multiple filters"""
1745+
1746+
# pylint: disable=redefined-outer-name
1747+
1748+
responses.add(
1749+
responses.GET,
1750+
"{0}/Employees/$count?%24filter=ID%20ge%2023".format(service.url),
1751+
json=3,
1752+
status=200)
1753+
1754+
employees = service.entity_sets.Employees.get_entities()
1755+
request = employees.filter(ID__gte=23).count()
1756+
1757+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1758+
1759+
assert request.execute() == 3
1760+
1761+
1762+
@responses.activate
1763+
def test_count_with_chainable_filter_eq_operator(service):
1764+
"""Check getting $count with $filter with new filter syntax using multiple filters"""
1765+
1766+
# pylint: disable=redefined-outer-name
1767+
1768+
responses.add(
1769+
responses.GET,
1770+
"{0}/Employees/$count?%24filter=ID%20eq%2023".format(service.url),
1771+
json=3,
1772+
status=200)
1773+
1774+
employees = service.entity_sets.Employees.get_entities()
1775+
request = employees.filter(ID__eq=23).count()
1776+
1777+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1778+
1779+
assert request.execute() == 3
1780+
1781+
1782+
@responses.activate
1783+
def test_count_with_chainable_filter_in_operator(service):
1784+
"""Check getting $count with $filter in"""
1785+
1786+
# pylint: disable=redefined-outer-name
1787+
1788+
responses.add(
1789+
responses.GET,
1790+
"{0}/Employees/$count?$filter=ID%20eq%201%20or%20ID%20eq%202%20or%20ID%20eq%203".format(service.url),
1791+
json=3,
1792+
status=200)
1793+
1794+
employees = service.entity_sets.Employees.get_entities()
1795+
request = employees.filter(ID__in=[1,2,3]).count()
1796+
1797+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1798+
1799+
assert request.execute() == 3
1800+
1801+
1802+
@responses.activate
1803+
def test_count_with_chainable_filter_startswith_operator(service):
1804+
"""Check getting $count with $filter in"""
1805+
1806+
# pylint: disable=redefined-outer-name
1807+
1808+
responses.add(
1809+
responses.GET,
1810+
"{0}/Employees/$count?$filter=startswith%28NickName%2C%20%27Tim%27%29%20eq%20true".format(service.url),
1811+
json=3,
1812+
status=200)
1813+
1814+
employees = service.entity_sets.Employees.get_entities()
1815+
request = employees.filter(NickName__startswith="Tim").count()
1816+
1817+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1818+
1819+
assert request.execute() == 3
1820+
1821+
1822+
@responses.activate
1823+
def test_count_with_chainable_filter_endswith_operator(service):
1824+
"""Check getting $count with $filter in"""
1825+
1826+
# pylint: disable=redefined-outer-name
1827+
1828+
responses.add(
1829+
responses.GET,
1830+
"{0}/Employees/$count?$filter=endswith%28NickName%2C%20%27othy%27%29%20eq%20true".format(service.url),
1831+
json=3,
1832+
status=200)
1833+
1834+
employees = service.entity_sets.Employees.get_entities()
1835+
request = employees.filter(NickName__endswith="othy").count()
1836+
1837+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1838+
1839+
assert request.execute() == 3
1840+
1841+
1842+
@responses.activate
1843+
def test_count_with_chainable_filter_length_operator(service):
1844+
"""Check getting $count with $filter in"""
1845+
1846+
# pylint: disable=redefined-outer-name
1847+
1848+
responses.add(
1849+
responses.GET,
1850+
"{0}/Employees/$count?$filter=length%28NickName%29%20eq%206".format(service.url),
1851+
json=3,
1852+
status=200)
1853+
1854+
employees = service.entity_sets.Employees.get_entities()
1855+
request = employees.filter(NickName__length=6).count()
1856+
1857+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1858+
1859+
assert request.execute() == 3
1860+
1861+
1862+
@responses.activate
1863+
def test_count_with_chainable_filter_length_operator_as_string(service):
1864+
"""Check getting $count with $filter in"""
1865+
1866+
# pylint: disable=redefined-outer-name
1867+
1868+
responses.add(
1869+
responses.GET,
1870+
"{0}/Employees/$count?$filter=length%28NickName%29%20eq%206".format(service.url),
1871+
json=3,
1872+
status=200)
1873+
1874+
employees = service.entity_sets.Employees.get_entities()
1875+
request = employees.filter(NickName__length="6").count()
1876+
1877+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1878+
1879+
assert request.execute() == 3
1880+
1881+
1882+
@responses.activate
1883+
def test_count_with_chainable_filter_contains_operator(service):
1884+
"""Check getting $count with $filter in"""
1885+
1886+
# pylint: disable=redefined-outer-name
1887+
1888+
responses.add(
1889+
responses.GET,
1890+
"{0}/Employees/$count?$filter=substringof%28%27Tim%27%2C%20NickName%29%20eq%20true".format(service.url),
1891+
json=3,
1892+
status=200)
1893+
1894+
employees = service.entity_sets.Employees.get_entities()
1895+
request = employees.filter(NickName__contains="Tim").count()
1896+
1897+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1898+
1899+
assert request.execute() == 3
1900+
1901+
1902+
@responses.activate
1903+
def test_count_with_chainable_filter_range_operator(service):
1904+
"""Check getting $count with $filter in"""
1905+
1906+
# pylint: disable=redefined-outer-name
1907+
1908+
responses.add(
1909+
responses.GET,
1910+
"{0}/Employees/$count?$filter=ID%20gte%2020%20and%20ID%20lte%2050".format(service.url),
1911+
json=3,
1912+
status=200)
1913+
1914+
employees = service.entity_sets.Employees.get_entities()
1915+
request = employees.filter(ID__range=(20, 50)).count()
1916+
1917+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1918+
1919+
assert request.execute() == 3
1920+
1921+
1922+
@responses.activate
1923+
def test_count_with_chainable_filter_multiple(service):
1924+
"""Check getting $count with $filter with new filter syntax using multiple filters"""
1925+
1926+
# pylint: disable=redefined-outer-name
1927+
1928+
responses.add(
1929+
responses.GET,
1930+
"{0}/Employees/$count?%24filter=ID%20eq%2023%20and%20NickName%20eq%20%27Steve%27".format(service.url),
1931+
json=3,
1932+
status=200)
1933+
1934+
employees = service.entity_sets.Employees.get_entities()
1935+
request = employees.filter(ID=23, NickName="Steve").count()
1936+
1937+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1938+
1939+
assert request.execute() == 3
1940+
1941+
1942+
@responses.activate
1943+
def test_count_with_chainable_filter_or(service):
1944+
"""Check getting $count with $filter with FilterExpression syntax or"""
1945+
from pyodata.v2.service import FilterExpression as Q
1946+
# pylint: disable=redefined-outer-name
1947+
1948+
responses.add(
1949+
responses.GET,
1950+
"{0}/Employees/$count?$filter=%28ID%20eq%2023%20and%20NickName%20eq%20%27Steve%27%29%20or%20%28ID%20eq%2025%20and%20NickName%20eq%20%27Tim%27%29".format(service.url),
1951+
json=3,
1952+
status=200)
1953+
1954+
employees = service.entity_sets.Employees.get_entities()
1955+
request = employees.filter(Q(ID=23, NickName="Steve") | Q(ID=25, NickName="Tim")).count()
1956+
1957+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1958+
1959+
assert request.execute() == 3
1960+
1961+
@responses.activate
1962+
def test_count_with_multiple_chainable_filters_startswith(service):
1963+
"""Check getting $count with $filter calling startswith"""
1964+
from pyodata.v2.service import FilterExpression as Q
1965+
# pylint: disable=redefined-outer-name
1966+
1967+
responses.add(
1968+
responses.GET,
1969+
"{0}/Employees/$count?$filter=%28ID%20eq%2023%20and%20startswith%28NickName%2C%20%27Ste%27%29%20eq%20true%29%20or%20%28ID%20eq%2025%20and%20NickName%20eq%20%27Tim%27%29".format(service.url),
1970+
json=3,
1971+
status=200)
1972+
1973+
employees = service.entity_sets.Employees.get_entities()
1974+
request = employees.filter(Q(ID=23, NickName__startswith="Ste") | Q(ID=25, NickName="Tim")).count()
1975+
1976+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
1977+
1978+
assert request.execute() == 3
1979+
1980+
1981+
@responses.activate
1982+
def test_count_with_chainable_filters_invalid_property_lookup(service):
1983+
"""Check getting $count with $filter calling startswith"""
1984+
# pylint: disable=redefined-outer-name
1985+
1986+
employees = service.entity_sets.Employees.get_entities()
1987+
with pytest.raises(ValueError) as ex:
1988+
request = employees.filter(Foo="Bar")
1989+
1990+
assert str(ex.value) == '"Foo" is not a valid property or operator'
1991+
1992+
1993+
@responses.activate
1994+
def test_count_with_chainable_filters_invalid_operator_lookup(service):
1995+
"""Check getting $count with $filter calling startswith"""
1996+
# pylint: disable=redefined-outer-name
1997+
1998+
employees = service.entity_sets.Employees.get_entities()
1999+
with pytest.raises(ValueError) as ex:
2000+
request = employees.filter(NickName__foo="Bar")
2001+
2002+
assert str(ex.value) == '"foo" is not a valid property or operator'
2003+
2004+
2005+
@responses.activate
2006+
def test_count_with_chained_filters(service):
2007+
"""Check getting $count with chained filters"""
2008+
2009+
# pylint: disable=redefined-outer-name
2010+
2011+
responses.add(
2012+
responses.GET,
2013+
"{0}/Employees/$count?$filter=ID%20gte%2020%20and%20ID%20lte%2050%20and%20NickName%20eq%20%27Tim%27".format(service.url),
2014+
json=3,
2015+
status=200)
2016+
2017+
employees = service.entity_sets.Employees.get_entities()
2018+
request = employees.filter(ID__range=(20, 50)).filter(NickName="Tim").count()
2019+
2020+
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
2021+
2022+
assert request.execute() == 3
2023+
2024+
16622025
@responses.activate
16632026
def test_create_entity_with_datetime(service):
16642027
"""

0 commit comments

Comments
 (0)
Please sign in to comment.