Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ with open('redacted_file.pdf','wb') as of:

The ```download_redacted_file``` method takes similar arguments to the ```redact()``` method. It also supports a ```generator_config``` parameter to adjust which entities are tokenized and synthesized.

When you no longer need the file, permanently delete the uploaded file and everything Textual derived from it:

```python
textual_ner.hard_delete_redacted_file(ref_id)
```

### Consistency

When entities are tokenized, the tokenized values are unique to the original value. A given entity always generates to the same unique token. To map a token back to its original value, use the ```unredact``` function call.
Expand Down
13 changes: 13 additions & 0 deletions docs/source/redact/redacting_files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ To redact an individual file:
with open('<Redacted file name>','wb') as redacted_file:
redacted_file.write(new_bytes)

Hard delete a redacted file
---------------------------

Textual keeps the uploaded file and the data that it derives from the file, such as detected entities and OCR results, so that you can download the redacted file again with different settings.

When you no longer need the file, use ``hard_delete_redacted_file`` to permanently delete the uploaded file, its redaction job, and all of the derived data. After you delete the file, you can no longer use the job identifier to download it.

.. code-block:: python

redact.hard_delete_redacted_file(j)

You can only delete a file after its redaction job finishes. If the file is still being processed, ``hard_delete_redacted_file`` raises ``FileNotReadyForDeletion``.

Configure how to handle specific entity types
----------------------------------------------

Expand Down
48 changes: 48 additions & 0 deletions tests/tests/redact_tests/test_hard_delete_redacted_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Unit tests for TextualNer.hard_delete_redacted_file. These do not require a running Textual server."""

from unittest.mock import patch

import pytest
import requests

from tonic_textual.classes.tonic_exception import FileNotReadyForDeletion
from tonic_textual.redact_api import TextualNer


def _response(status_code: int) -> requests.Response:
response = requests.Response()
response.status_code = status_code
response._content = b""
response.url = "http://localhost/api/unattachedfile/job-id"
response.request = requests.Request("DELETE", response.url).prepare()
return response


@pytest.fixture
def ner():
return TextualNer("http://localhost", api_key="fake-key")


def test_sends_delete_for_job(ner):
with patch("tonic_textual.classes.httpclient.requests.delete", return_value=_response(204)) as delete:
assert ner.hard_delete_redacted_file("job-id") is None

assert delete.call_args.args[0] == "http://localhost/api/unattachedfile/job-id"


def test_still_processing_raises_file_not_ready_for_deletion(ner):
with (
patch("tonic_textual.classes.httpclient.requests.delete", return_value=_response(409)),
pytest.raises(FileNotReadyForDeletion),
):
ner.hard_delete_redacted_file("job-id")


def test_unknown_job_raises_http_error(ner):
with (
patch("tonic_textual.classes.httpclient.requests.delete", return_value=_response(404)),
pytest.raises(requests.exceptions.HTTPError) as error,
):
ner.hard_delete_redacted_file("job-id")

assert error.value.response.status_code == 404
9 changes: 9 additions & 0 deletions tonic_textual/classes/tonic_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ def __init__(self, msg):
super().__init__(msg)


class FileNotReadyForDeletion(Exception):
"""
Raised when you make a request to delete a file that is still being processed
"""

def __init__(self, msg):
super().__init__(msg)


class AudioTranscriptionResultAlreadyRetrieved(Exception):
"""
Raised when you try to retrieve a transcription result that was previously retrieved and hence deleted
Expand Down
34 changes: 34 additions & 0 deletions tonic_textual/redact_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from tonic_textual.classes.tonic_exception import (
BadArgumentsException,
DatasetNameAlreadyExists,
FileNotReadyForDeletion,
FileNotReadyForDownload,
InvalidJsonForRedactionRequest,
)
Expand Down Expand Up @@ -1374,6 +1375,39 @@ def download_redacted_file(
"This is likely due to a high service load. Try again later."
)

def hard_delete_redacted_file(self, job_id: str) -> None:
"""
Hard delete a file that was uploaded for redaction

Deletes the uploaded file, its redaction job, and all of the data that
Textual derived from it, such as detected entities and OCR results.
After deletion, the job identifier can no longer be used to download
the redacted file. This cannot be undone.

Parameters
--------
job_id: str
The identifier of the redaction job, as returned by
start_file_redaction.

Raises
------
FileNotReadyForDeletion
The file is still being processed. Wait for the job to finish
before you delete the file.
requests.exceptions.HTTPError
A redaction job with the specified identifier was not found (404).
"""

try:
self.client.http_delete(f"/api/unattachedfile/{job_id}")
except requests.exceptions.HTTPError as err:
if err.response is not None and err.response.status_code == 409:
raise FileNotReadyForDeletion(
"The file is still being processed. Wait for the job to finish before you delete the file."
) from err
raise

# --- Model-Based Custom Entities ---

def create_model_entity(
Expand Down
Loading