Repository: SAP/python-pyodata

Issue:
Function to delete entity?
Is there a delete entity function?

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=h1) Report
> Merging [#57](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=desc) into [master](https://codecov.io/gh/SAP/python-pyodata/commit/00667d982a54063c18669582dd8eb16a9b2192f4?src=pr&el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/SAP/python-pyodata/pull/57/graphs/tree.svg?width=650&token=nInJCpPrP3&height=150&src=pr)](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #57      +/-   ##
==========================================
+ Coverage   91.25%   91.32%   +0.07%     
==========================================
  Files           6        6              
  Lines        2207     2226      +19     
==========================================
+ Hits         2014     2033      +19     
  Misses        193      193
```


| [Impacted Files](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyodata/v2/service.py](https://codecov.io/gh/SAP/python-pyodata/pull/57/diff?src=pr&el=tree#diff-cHlvZGF0YS92Mi9zZXJ2aWNlLnB5) | `89.7% <100%> (+0.28%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=footer). Last update [00667d9...e81d166](https://codecov.io/gh/SAP/python-pyodata/pull/57?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/index.rst b/docs/index.rst
index 2b74cdb..21de391 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -50,6 +50,7 @@ The User Guide
    usage/initialization.rst
    usage/querying.rst
    usage/creating.rst
+   usage/deleting.rst
    usage/function_imports.rst
    usage/metadata.rst
    usage/advanced.rst
diff --git a/docs/usage/README.md b/docs/usage/README.md
index 52ba147..d31f172 100644
--- a/docs/usage/README.md
+++ b/docs/usage/README.md
@@ -4,6 +4,7 @@ The User Guide
 * [Initialization](initialization.rst)
 * [Querying](querying.rst)
 * [Creating](creating.rst)
+* [Deleting](deleting.rst)
 * [Function Imports](function_imports.rst)
 * [Metadata](metadata.rst)
 * [Advanced](advanced.rst)
diff --git a/docs/usage/deleting.rst b/docs/usage/deleting.rst
new file mode 100644
index 0000000..f683fbf
--- /dev/null
+++ b/docs/usage/deleting.rst
@@ -0,0 +1,44 @@
+Deleting
+========
+
+.. _CSRF: https://en.wikipedia.org/wiki/Cross-site_request_forgery
+.. _Requests: https://2.python-requests.org/en/master/
+
+The delete action executes the HTTP method DELETE which is usually protected by
+CSRF_ and therefore you must make some effort to initialize your HTTP Session
+to send DELETE requests acceptable by the remote server.
+
+Let's assume you use the python library Requests_
+
+.. code-block:: python
+
+    import pyodata
+    import requests
+
+    SERVICE_URL = 'http://example.io/TheServiceRoot/'
+
+    session = requests.Session()
+    response = session.head(SERVICE_URL, headers={'x-csrf-token': 'fetch'})
+    token = response.headers.get('x-csrf-token', '')
+    session.headers.update({'x-csrf-token': token})
+
+    theservice = pyodata.Client(SERVICE_URL, session)
+
+
+Deleting an entity
+------------------
+
+You can either delete entity by passing its PropertyRef value to the delete function
+
+.. code-block:: python
+
+    request = service.entity_sets.Employees.delete_entity(23)
+    request.execute()
+
+or by passing the EntityKey object
+
+.. code-block:: python
+
+    key = EntityKey(service.schema.entity_type('Employee'), ID=23)
+    request = service.entity_sets.Employees.delete_entity(key=key)
+    request.execute()
\ No newline at end of file
diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py
index 6190dab..907a39d 100644
--- a/pyodata/v2/service.py
+++ b/pyodata/v2/service.py
@@ -480,6 +480,25 @@ class EntityCreateRequest(ODataHttpRequest):
         return self
 
 
+class EntityDeleteRequest(ODataHttpRequest):
+    """Used for deleting entity (DELETE operations on a single entity)"""
+
+    def __init__(self, url, connection, handler, entity_set, entity_key):
+        super(EntityDeleteRequest, self).__init__(url, connection, handler)
+        self._logger = logging.getLogger(LOGGER_NAME)
+        self._entity_set = entity_set
+        self._entity_key = entity_key
+
+        self._logger.debug('New instance of EntityDeleteRequest for entity type: %s', entity_set.entity_type.name)
+
+    def get_path(self):
+        return self._entity_set.name + self._entity_key.to_key_string()
+
+    def get_method(self):
+        # pylint: disable=no-self-use
+        return 'DELETE'
+
+
 class EntityModifyRequest(ODataHttpRequest):
     """Used for modyfing entities (UPDATE/MERGE operations on a single entity)
 
@@ -1131,6 +1150,25 @@ class EntitySetProxy:
         return EntityModifyRequest(self._service.url, self._service.connection, update_entity_handler, self._entity_set,
                                    entity_key)
 
+    def delete_entity(self, key: EntityKey = None, **kwargs):
+        """Delete the entity"""
+
+        def delete_entity_handler(response):
+            """Check if entity deletion was successful"""
+
+            if response.status_code != 204:
+                raise HttpError(f'HTTP POST for Entity delete {self._name} '
+                                f'failed with status code {response.status_code}',
+                                response)
+
+        if key is not None and isinstance(key, EntityKey):
+            entity_key = key
+        else:
+            entity_key = EntityKey(self._entity_set.entity_type, key, **kwargs)
+
+        return EntityDeleteRequest(self._service.url, self._service.connection, delete_entity_handler, self._entity_set,
+                                   entity_key)
+
 
 # pylint: disable=too-few-public-methods
 class EntityContainer:


Test patch:
diff --git a/tests/test_service_v2.py b/tests/test_service_v2.py
index 3a508c8..cdcc1b8 100644
--- a/tests/test_service_v2.py
+++ b/tests/test_service_v2.py
@@ -590,6 +590,46 @@ def test_update_entity(service):
     request.execute()
 
 
+@responses.activate
+def test_delete_entity(service):
+    """Check deleting of entity"""
+
+    responses.add(responses.DELETE, f"{service.url}/Employees(23)", status=204)
+    request = service.entity_sets.Employees.delete_entity(23)
+
+    assert isinstance(request, pyodata.v2.service.EntityDeleteRequest)
+    assert request.execute() is None
+
+
+@responses.activate
+def test_delete_entity_with_key(service):
+    """Check deleting of entity with key"""
+
+    responses.add(responses.DELETE, f"{service.url}/Employees(ID=23)", status=204)
+    key = EntityKey(service.schema.entity_type('Employee'), ID=23)
+    request = service.entity_sets.Employees.delete_entity(key=key)
+
+    assert isinstance(request, pyodata.v2.service.EntityDeleteRequest)
+    assert request.execute() is None
+
+
+@responses.activate
+def test_delete_entity_http_error(service):
+    """Check if error is raisen when deleting unknown entity"""
+
+    responses.add(responses.DELETE, f"{service.url}/Employees(ID=23)", status=404)
+    key = EntityKey(service.schema.entity_type('Employee'), ID=23)
+    request = service.entity_sets.Employees.delete_entity(key=key)
+
+    assert isinstance(request, pyodata.v2.service.EntityDeleteRequest)
+
+    with pytest.raises(HttpError) as caught_ex:
+        request.execute()
+
+    assert str(caught_ex.value).startswith('HTTP POST for Entity delete')
+    assert caught_ex.value.response.status_code == 404
+
+
 def test_update_entity_with_entity_key(service):
     """Make sure the method update_entity handles correctly the parameter key which is EntityKey"""