Repository: argilla-io/argilla

Issue:
feat: Set a default value to the `event_timestamp` field
**Describe the bug**
After going through the quickstart. The Kibana records don't contain time information, besides, `last_updated`. 

**To Reproduce**
Steps to reproduce the behavior:
1. Initialize your Argilla with Kibana
2. Upload the [quickstart](https://docs.argilla.io/en/latest/getting_started/quickstart.html#). 
3. Create a Kibana index pattern based on `event_timestamp`
4. No record show due to empty field.

**Expected behavior**
I would expect to have this set to the same date as `event_timestamp`, until someone has annotated the data and therefore updated.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**
 - OS [e.g. iOS]: MacOS
 - Browser [e.g. chrome, safari]: Firefox
 - Argilla Version [e.g. 1.0.0]: 1.0.1
 - ElasticSearch Version [e.g. 7.10.2]: 7.10.2
 - Docker Image (optional) [e.g. argilla:v1.0.0]: argilla:v1.0.1

**Additional context**
Maybe we can align this with other kibana issues #1803.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/argilla-io/argilla/pull/1970?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io) Report
Base: **94.57**% // Head: **94.52**% // Decreases project coverage by **`-0.04%`** :warning:
> Coverage data is based on head [(`a4e17e4`)](https://codecov.io/gh/argilla-io/argilla/pull/1970?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io) compared to base [(`7d5a34a`)](https://codecov.io/gh/argilla-io/argilla/commit/7d5a34a8565491f4c6738e270ddf2867fbb01334?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #1970      +/-   ##
===========================================
- Coverage    94.57%   94.52%   -0.05%     
===========================================
  Files          150      150              
  Lines         7091     7092       +1     
===========================================
- Hits          6706     6704       -2     
- Misses         385      388       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `94.52% <100.00%> (-0.05%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/argilla-io/argilla/pull/1970?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io) | Coverage Δ | |
|---|---|---|
| [src/argilla/client/models.py](https://codecov.io/gh/argilla-io/argilla/pull/1970/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io#diff-c3JjL2FyZ2lsbGEvY2xpZW50L21vZGVscy5weQ==) | `94.17% <100.00%> (+0.03%)` | :arrow_up: |
| [...gilla/labeling/text\_classification/label\_errors.py](https://codecov.io/gh/argilla-io/argilla/pull/1970/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io#diff-c3JjL2FyZ2lsbGEvbGFiZWxpbmcvdGV4dF9jbGFzc2lmaWNhdGlvbi9sYWJlbF9lcnJvcnMucHk=) | `86.74% <0.00%> (-3.62%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/argilla-io/argilla/pull/1970?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argilla-io).


Patch:
diff --git a/docs/_source/guides/features/datasets.ipynb b/docs/_source/guides/features/datasets.ipynb
index c09371468..a429f4173 100644
--- a/docs/_source/guides/features/datasets.ipynb
+++ b/docs/_source/guides/features/datasets.ipynb
@@ -56,6 +56,70 @@
     "The Dataset classes do some extra checks for you, to make sure you do not mix record types when appending or indexing into a dataset. "
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "ee0f9b20",
+   "metadata": {},
+   "source": [
+    "## Updating a Dataset\n",
+    "\n",
+    "Argilla datasets have certain *settings* that you can configure via the `rg.*Settings` classes, for example `rg.TextClassificationSettings`.\n",
+    "\n",
+    "### Define a labeling schema\n",
+    "\n",
+    "You can define a labeling schema for your Argilla dataset, which fixes the allowed labels for your predictions and annotations.\n",
+    "Once you set a labeling schema, each time you log to the corresponding dataset, Argilla will perform validations of the added predictions and annotations to make sure they comply with the schema."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bce01a6b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import argilla as rg\n",
+    "\n",
+    "# Define labeling schema\n",
+    "settings = rg.TextClassificationSettings(label_schema=[\"A\", \"B\", \"C\"])\n",
+    "\n",
+    "# Apply settings to a new or already existing dataset\n",
+    "rg.configure_dataset(name=\"my_dataset\", settings=settings)\n",
+    "\n",
+    "# Logging to the newly created dataset triggers the validation checks\n",
+    "rg.log(rg.TextClassificationRecord(text=\"text\", annotation=\"D\"), \"my_dataset\")\n",
+    "# BadRequestApiError: Argilla server returned an error with http status: 400\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "eed77791",
+   "metadata": {},
+   "source": [
+    "### Updating Records\n",
+    "\n",
+    "It is possible to update records from your Argilla datasets using our Python API. This approach works the same way as an upsert in a normal database, based on the record `id`. You can update any arbitrary parameters and they will be over-written if you use the `id` of the original record."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "866e11c7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import argilla as rg\n",
+    "\n",
+    "# read all records in the dataset or define a specific search via the `query` parameter\n",
+    "record = rg.load(\"my_first_dataset\")\n",
+    "\n",
+    "# modify first record metadata (if no previous metadata dict you might need to create it)\n",
+    "record[0].metadata[\"my_metadata\"] = \"im a new value\"\n",
+    "\n",
+    "# log record to update it, this will keep everything but add my_metadata field and value\n",
+    "rg.log(name=\"my_first_dataset\", records=record[0])"
+   ]
+  },
   {
    "cell_type": "markdown",
    "id": "df88889b-12f4-472f-bcbe-fb47be475d02",
@@ -284,47 +348,6 @@
     "# Output:\n",
     "# {..., 'tokens': ['I', 'live', 'in', 'Madrid'], 'ner_tags': [0, 0, 0, 1], ...}\n"
    ]
-  },
-  {
-   "cell_type": "markdown",
-   "id": "83522c04",
-   "metadata": {},
-   "source": [
-    "## Dataset settings\n",
-    "\n",
-    "Argilla datasets have certain *settings* that you can configure via the `rg.*Settings` classes, for example `rg.TextClassificationSettings`."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "id": "51750c7c",
-   "metadata": {},
-   "source": [
-    "### Define a labeling schema\n",
-    "\n",
-    "You can define a labeling schema for your Argilla dataset, which fixes the allowed labels for your predictions and annotations.\n",
-    "Once you set a labeling schema, each time you log to the corresponding dataset, Argilla will perform validations of the added predictions and annotations to make sure they comply with the schema."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "225e783b",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import argilla as rg\n",
-    "\n",
-    "# Define labeling schema\n",
-    "settings = rg.TextClassificationSettings(label_schema=[\"A\", \"B\", \"C\"])\n",
-    "\n",
-    "# Apply settings to a new or already existing dataset\n",
-    "rg.configure_dataset(name=\"my_dataset\", settings=settings)\n",
-    "\n",
-    "# Logging to the newly created dataset triggers the validation checks\n",
-    "rg.log(rg.TextClassificationRecord(text=\"text\", annotation=\"D\"), \"my_dataset\")\n",
-    "# BadRequestApiError: Argilla server returned an error with http status: 400\n"
-   ]
   }
  ],
  "metadata": {
diff --git a/src/argilla/client/models.py b/src/argilla/client/models.py
index f8f3b0e74..11a2a70f3 100644
--- a/src/argilla/client/models.py
+++ b/src/argilla/client/models.py
@@ -50,8 +50,9 @@ class _Validators(BaseModel):
 
         if default_length_exceeded:
             message = (
-                "Some metadata values could exceed the max length. For those cases, values will be"
-                f" truncated by keeping only the last {DEFAULT_MAX_KEYWORD_LENGTH} characters. "
+                "Some metadata values could exceed the max length. For those cases,"
+                " values will be truncated by keeping only the last"
+                f" {DEFAULT_MAX_KEYWORD_LENGTH} characters. "
                 + _messages.ARGILLA_METADATA_FIELD_WARNING_MESSAGE
             )
             warnings.warn(message, UserWarning)
@@ -84,11 +85,14 @@ class _Validators(BaseModel):
             )
         return v
 
-    @validator("event_timestamp", check_fields=False)
-    def _nat_to_none(cls, v):
-        """Converts pandas `NaT`s to `None`s"""
+    @validator("event_timestamp", check_fields=False, always=True)
+    def _nat_to_none_and_one_to_now(cls, v):
+        """Converts pandas `NaT`s to `None`s and None´s to datetime.now()"""
         if v is pd.NaT:
-            return None
+            v = None
+
+        v = v or datetime.datetime.now()
+
         return v
 
     @root_validator
@@ -163,7 +167,7 @@ class TextClassificationRecord(_Validators):
             The status of the record. Options: 'Default', 'Edited', 'Discarded', 'Validated'.
             If an annotation is provided, this defaults to 'Validated', otherwise 'Default'.
         event_timestamp:
-            The timestamp of the record.
+            The timestamp for the creation of the record. Defaults to `datetime.datetime.now()`.
         metrics:
             READ ONLY! Metrics at record level provided by the server when using `rg.load`.
             This attribute will be ignored when using `rg.log`.
@@ -274,7 +278,7 @@ class TokenClassificationRecord(_Validators):
             The status of the record. Options: 'Default', 'Edited', 'Discarded', 'Validated'.
             If an annotation is provided, this defaults to 'Validated', otherwise 'Default'.
         event_timestamp:
-            The timestamp of the record.
+            The timestamp for the creation of the record. Defaults to `datetime.datetime.now()`.
         metrics:
             READ ONLY! Metrics at record level provided by the server when using `rg.load`.
             This attribute will be ignored when using `rg.log`.
@@ -307,7 +311,6 @@ class TokenClassificationRecord(_Validators):
 
     metrics: Optional[Dict[str, Any]] = None
     search_keywords: Optional[List[str]] = None
-
     _span_utils: SpanUtils = PrivateAttr()
 
     def __init__(
@@ -491,7 +494,7 @@ class TextGenerationRecord(_Validators):
             The status of the record. Options: 'Default', 'Edited', 'Discarded', 'Validated'.
             If an annotation is provided, this defaults to 'Validated', otherwise 'Default'.
         event_timestamp:
-            The timestamp of the record.
+            The timestamp for the creation of the record. Defaults to `datetime.datetime.now()`.
         metrics:
             READ ONLY! Metrics at record level provided by the server when using `rg.load`.
             This attribute will be ignored when using `rg.log`.


Test patch:
diff --git a/tests/client/test_models.py b/tests/client/test_models.py
index 520453dcc..6d5d4b052 100644
--- a/tests/client/test_models.py
+++ b/tests/client/test_models.py
@@ -53,13 +53,18 @@ def test_text_classification_record(annotation, status, expected_status):
 
 
 def test_text_classification_input_string():
-    assert TextClassificationRecord(text="A text") == TextClassificationRecord(
-        inputs=dict(text="A text")
+    event_timestamp = datetime.datetime.now()
+    assert TextClassificationRecord(
+        text="A text", event_timestamp=event_timestamp
+    ) == TextClassificationRecord(
+        inputs=dict(text="A text"), event_timestamp=event_timestamp
     )
 
     assert TextClassificationRecord(
-        inputs=["A text", "another text"]
-    ) == TextClassificationRecord(inputs=dict(text=["A text", "another text"]))
+        inputs=["A text", "another text"], event_timestamp=event_timestamp
+    ) == TextClassificationRecord(
+        inputs=dict(text=["A text", "another text"]), event_timestamp=event_timestamp
+    )
 
 
 def test_text_classification_text_inputs():
@@ -71,21 +76,31 @@ def test_text_classification_text_inputs():
 
     with pytest.warns(
         FutureWarning,
-        match="the `inputs` argument of the `TextClassificationRecord` will not accept strings.",
+        match=(
+            "the `inputs` argument of the `TextClassificationRecord` will not accept"
+            " strings."
+        ),
     ):
         TextClassificationRecord(inputs="mock")
 
-    assert TextClassificationRecord(text="mock") == TextClassificationRecord(
-        inputs={"text": "mock"}
+    event_timestamp = datetime.datetime.now()
+    assert TextClassificationRecord(
+        text="mock", event_timestamp=event_timestamp
+    ) == TextClassificationRecord(
+        inputs={"text": "mock"}, event_timestamp=event_timestamp
     )
 
-    assert TextClassificationRecord(inputs=["mock"]) == TextClassificationRecord(
-        inputs={"text": ["mock"]}
+    assert TextClassificationRecord(
+        inputs=["mock"], event_timestamp=event_timestamp
+    ) == TextClassificationRecord(
+        inputs={"text": ["mock"]}, event_timestamp=event_timestamp
     )
 
     assert TextClassificationRecord(
-        text="mock", inputs={"text": "mock"}
-    ) == TextClassificationRecord(inputs={"text": "mock"})
+        text="mock", inputs={"text": "mock"}, event_timestamp=event_timestamp
+    ) == TextClassificationRecord(
+        inputs={"text": "mock"}, event_timestamp=event_timestamp
+    )
 
     rec = TextClassificationRecord(text="mock")
     with pytest.raises(AttributeError, match="You cannot assign a new value to `text`"):
@@ -133,8 +148,10 @@ def test_token_classification_with_tokens_and_tags(tokens, tags, annotation):
 def test_token_classification_validations():
     with pytest.raises(
         AssertionError,
-        match="Missing fields: "
-        "At least one of `text` or `tokens` argument must be provided!",
+        match=(
+            "Missing fields: "
+            "At least one of `text` or `tokens` argument must be provided!"
+        ),
     ):
         TokenClassificationRecord()
 
@@ -142,15 +159,19 @@ def test_token_classification_validations():
     annotation = [("test", 0, 4)]
     with pytest.raises(
         AssertionError,
-        match="Missing field `text`: "
-        "char level spans must be provided with a raw text sentence",
+        match=(
+            "Missing field `text`: "
+            "char level spans must be provided with a raw text sentence"
+        ),
     ):
         TokenClassificationRecord(tokens=tokens, annotation=annotation)
 
     with pytest.raises(
         AssertionError,
-        match="Missing field `text`: "
-        "char level spans must be provided with a raw text sentence",
+        match=(
+            "Missing field `text`: "
+            "char level spans must be provided with a raw text sentence"
+        ),
     ):
         TokenClassificationRecord(tokens=tokens, prediction=annotation)
 
@@ -259,12 +280,20 @@ def test_forbid_extra():
         MockRecord(mock="mock", extra_argument="mock")
 
 
-def test_nat_to_none():
+def test_none_to_datetime():
+    class MockRecord(_Validators):
+        event_timestamp: Optional[datetime.datetime] = None
+
+    record = MockRecord()
+    assert isinstance(record.event_timestamp, datetime.datetime)
+
+
+def test_nat_to_none_to_datetime():
     class MockRecord(_Validators):
         event_timestamp: Optional[datetime.datetime] = None
 
     record = MockRecord(event_timestamp=pd.NaT)
-    assert record.event_timestamp is None
+    assert isinstance(record.event_timestamp, datetime.datetime)
 
 
 @pytest.mark.parametrize(