Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- service: serialize iterable selections as comma-separated fields for entity and collection requests, including wildcard selections (#71, #73).

- service: `update_entity` accepts `200 OK` as well as `204 No Content`, so updates against services such as SAP SuccessFactors no longer raise `HttpError` (#136) - Sena Köse

## [1.12.1]
Expand Down
13 changes: 13 additions & 0 deletions docs/usage/querying.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ Print unique identification (Id) and last name of all employees:
print(employee.EmployeeID, employee.LastName)


Selecting properties
--------------------

Both single-entity and entity-set requests accept comma-separated text or an
iterable of property names. Use ``'*'`` to request all properties, and ``None``
to remove an existing selection:

.. code-block:: python

employee = northwind.entity_sets.Employees.get_entity(1).select(['EmployeeID', 'LastName']).execute()
employees = northwind.entity_sets.Employees.get_entities().select('*').execute()


Get entities matching a filter
------------------------------

Expand Down
15 changes: 11 additions & 4 deletions pyodata/v2/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
HTTP_CODE_NO_CONTENT = 204


def _normalize_select(select):
"""Convert an iterable of selection clauses to its query representation."""
if select is None or isinstance(select, str):
return select
return ','.join(select)


def urljoin(*path):
"""Joins the passed string parts into a one string url"""

Expand Down Expand Up @@ -397,9 +404,9 @@ def nav(self, nav_property):
def select(self, select):
"""Specifies a subset of properties to return.

@param select a comma-separated list of selection clauses
@param select comma-separated text or an iterable of selection clauses
"""
self._select = select
self._select = _normalize_select(select)
return self

def expand(self, expand):
Expand Down Expand Up @@ -710,8 +717,8 @@ def order_by(self, order_by):
return self

def select(self, select):
"""Sets the selection clauses."""
self._select = select
"""Sets selection clauses from comma-separated text or an iterable."""
self._select = _normalize_select(select)
return self

def skip(self, skip):
Expand Down
44 changes: 43 additions & 1 deletion tests/test_service_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,48 @@
URL_ROOT = 'http://odatapy.example.com'


@pytest.mark.parametrize('single', [False, True])
@pytest.mark.parametrize('make_selection', [list, tuple, iter])
def test_select_iterable(service, single, make_selection):
"""Iterable selections are serialized as one comma-separated query value."""
request = (service.entity_sets.Employees.get_entity(23) if single
else service.entity_sets.Employees.get_entities())
assert request.select(make_selection(['NameFirst', 'NameLast'])) is request
assert request.get_query_params()['$select'] == 'NameFirst,NameLast'
# A generator must not be consumed again while building another request.
assert request.get_query_params()['$select'] == 'NameFirst,NameLast'


@pytest.mark.parametrize('single', [False, True])
def test_select_string_and_reset(service, single):
"""String selections and clearing an existing selection remain supported."""
request = (service.entity_sets.Employees.get_entity(23) if single
else service.entity_sets.Employees.get_entities())
assert request.select('NameFirst,NameLast') is request
assert request.get_query_params()['$select'] == 'NameFirst,NameLast'
assert request.select(None) is request
assert '$select' not in request.get_query_params()


@pytest.mark.parametrize('single', [False, True])
@pytest.mark.parametrize('selection', ['*', ['*'], ('*',), {'*'}])
def test_select_wildcard(service, single, selection):
"""Both request types preserve the OData all-properties selection."""
request = (service.entity_sets.Employees.get_entity(23) if single
else service.entity_sets.Employees.get_entities())
assert request.select(selection) is request
assert request.get_query_params()['$select'] == '*'


@pytest.mark.parametrize('single', [False, True])
def test_select_set(service, single):
"""Unordered selection inputs retain every requested property."""
request = (service.entity_sets.Employees.get_entity(23) if single
else service.entity_sets.Employees.get_entities())
request.select({'NameFirst', 'NameLast'})
assert set(request.get_query_params()['$select'].split(',')) == {'NameFirst', 'NameLast'}


@pytest.fixture
def service(schema):
"""Service fixture"""
Expand Down Expand Up @@ -3204,4 +3246,4 @@ def hook(response):

def test_service_without_response_hook_works(service):
"""response_hook defaults to None and does not affect normal operation"""
assert service.response_hook is None
assert service.response_hook is None