From 2d81f7671b656fa82f567714a2a6f758ebba877a Mon Sep 17 00:00:00 2001
From: Daniel Bimschas <daniel@bimschas.com>
Date: Sat, 7 Dec 2024 23:22:59 +0100
Subject: [PATCH] HOLI-10718: add updating Typesense

---
 events.py                          |  6 ++---
 main.py                            | 38 +++++++++++++++++++++++-------
 requirements.txt                   |  1 +
 terraform/environments/function.tf |  9 +++----
 test_main.py                       | 31 ++++++++++++++++++++----
 typesense_client.py                | 24 +++++++++++++++++++
 6 files changed, 89 insertions(+), 20 deletions(-)
 create mode 100644 typesense_client.py

diff --git a/events.py b/events.py
index 1819e3f..dabc674 100644
--- a/events.py
+++ b/events.py
@@ -27,15 +27,15 @@ class UserPayload:
 
 
 @dataclass
-class UserEvent(UserPayload):
+class UserEvent:
     event_type: str
     event_version: str
+    user: UserPayload
 
     def __init__(self, cloud_event: CloudEvent):
         self.event_type = get_event_type(cloud_event)
         self.event_version = get_event_version(cloud_event)
-        data = cloud_event.data['message']['data']
-        super().__init__(json.loads(base64.b64decode(data)))
+        self.user = UserPayload(json.loads(base64.b64decode(cloud_event.data['message']['data'])))
 
 @dataclass
 class UserNameUpdatedEvent(UserEvent):
diff --git a/main.py b/main.py
index 1ee6075..4550ddd 100644
--- a/main.py
+++ b/main.py
@@ -1,23 +1,45 @@
+import os
 import functions_framework
+
+from dataclasses import asdict
 from cloudevents.http.event import CloudEvent
+
 from events import UserNameUpdatedEvent, get_event_type, get_event_version, UserDeletedEvent
-from dataclasses import asdict
+from typesense_client import TypesenseClient
 
-def process_user_name_updated_event(event: UserNameUpdatedEvent):
+def process_user_name_updated_event(client: TypesenseClient, event: UserNameUpdatedEvent):
     print(f'process_user_name_updated_event: {asdict(event)}')
+    document = {
+        'id': f'profile_{event.user.id}',
+        'type': 'profile',
+        'title_de': event.user.name,
+        'title_en': event.user.name,
+        'description_de': None,  # TODO add on sending side
+        'description_en': None,  # TODO add on sending side
+        'location': None,  # TODO add on sending side
+        'location_lat_lng': None,  # TODO add on sending side
+        'image_url': event.user.avatar,
+        'link_locators': {
+            'profile': event.user.id
+        }
+    }
+    response = client.upsert(document)
+    print(f'process_user_name_updated_event: {response}')
 
-def process_user_deleted_event(event: UserDeletedEvent):
+def process_user_deleted_event(client: TypesenseClient, event: UserDeletedEvent):
     print(f'process_user_deleted_event: {asdict(event)}')
+    response = client.delete(f"profile_{event.user.id}")
+    print(f'process_user_deleted_event response: {response}')
 
-def process_event(event: CloudEvent):
+def process_event(client: TypesenseClient, event: CloudEvent):
     type_version = (get_event_type(event), get_event_version(event))
     match type_version:
         case ('UserNameUpdated', '1.0.0'):
-            return process_user_name_updated_event(UserNameUpdatedEvent(event))
+            process_user_name_updated_event(client, UserNameUpdatedEvent(event))
         case ('UserDeleted', '1.0.0'):
-            return process_user_deleted_event(UserDeletedEvent(event))
+            process_user_deleted_event(client, UserDeletedEvent(event))
 
 @functions_framework.cloud_event
 def process_message(event: CloudEvent):
-    #print(f"Received message: {event}")
-    process_event(event)
+    client = TypesenseClient()
+    process_event(client, event)
diff --git a/requirements.txt b/requirements.txt
index 6c9d154..3874904 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 functions_framework==3.8.2
 pytest==8.3.4
+typesense==0.21.0
\ No newline at end of file
diff --git a/terraform/environments/function.tf b/terraform/environments/function.tf
index 9bc1683..0f63338 100644
--- a/terraform/environments/function.tf
+++ b/terraform/environments/function.tf
@@ -39,10 +39,11 @@ resource "google_cloudfunctions2_function" "holi-search-integration" {
     max_instance_request_concurrency = 5
 
     environment_variables = {
-      ENVIRONMENT             = local.environment_name
-      TYPESENSE_HOST          = "t9vemwkibxajcorqp-1.a1.typesense.net"
-      TYPESENSE_PORT          = "443"
-      TYPESENSE_PROTOCOL      = "https"
+      ENVIRONMENT               = local.environment_name
+      TYPESENSE_HOST            = "t9vemwkibxajcorqp-1.a1.typesense.net"
+      TYPESENSE_PORT            = "443"
+      TYPESENSE_PROTOCOL        = "https"
+      TYPESENSE_COLLECTION_NAME = local.environment_name == "production" ? "production_all" : "staging_all"
     }
     vpc_connector                  = data.terraform_remote_state.holi_infra_state.outputs.vpc_access_connector_name
     vpc_connector_egress_settings  = "PRIVATE_RANGES_ONLY"
diff --git a/test_main.py b/test_main.py
index c136ddb..a5acd3d 100644
--- a/test_main.py
+++ b/test_main.py
@@ -1,9 +1,11 @@
 import base64
 import json
 
+from unittest.mock import patch
+
 from cloudevents.http import CloudEvent
 from cloudevents.http.event import CloudEvent
-from main import process_message
+from main import process_event
 
 attributes = {
     'id': '12833783708309476',
@@ -36,9 +38,28 @@ def message_data(event_type, event_version, data):
         }
 }
 
-def test_process_message():
-    process_message(CloudEvent(attributes, message_data('UserNameUpdated', '1.0.0', {'user': user_payload})))
-    process_message(CloudEvent(attributes, message_data('UserDeleted', '1.0.0', {'user': user_payload})))
+@patch('typesense.client.Client')
+def test_user_name_updated(mock_client):
+    process_event(mock_client, CloudEvent(attributes, message_data('UserNameUpdated', '1.0.0', {'user': user_payload})))
+    mock_client.upsert.assert_called_with({
+        'id': f'profile_{user_payload["id"]}',
+        'type': 'profile',
+        'title_de': user_payload['name'],
+        'title_en': user_payload['name'],
+        'description_de': None,  # TODO add on sending side
+        'description_en': None,  # TODO add on sending side
+        'location': None,  # TODO add on sending side
+        'location_lat_lng': None,  # TODO add on sending side
+        'image_url': user_payload['avatar'],
+        'link_locators': {
+            'profile': user_payload['id'],
+        }
+    })
+
+@patch('typesense.client.Client')
+def test_user_deleted(mock_client):
+    process_event(mock_client, CloudEvent(attributes, message_data('UserDeleted', '1.0.0', {'user': user_payload})))
+    mock_client.delete.assert_called_with(f"profile_{user_payload['id']}")
 
 if __name__ == '__main__':
-    test_process_message()
\ No newline at end of file
+    process_event()
\ No newline at end of file
diff --git a/typesense_client.py b/typesense_client.py
new file mode 100644
index 0000000..bd0f640
--- /dev/null
+++ b/typesense_client.py
@@ -0,0 +1,24 @@
+from typesense.client import Client
+
+# thin wrapper to simplify main and testing code
+class TypesenseClient(object):
+    client: Client
+
+    def __init__(self):
+        self.client = Client({
+            "api_key": (os.getenv("TYPESENSE_ADMIN_API_KEY")),
+            "nodes": [
+                {
+                    "host": (os.getenv("TYPESENSE_HOST")),
+                    "port": (os.getenv("TYPESENSE_PORT")),
+                    "protocol": (os.getenv("TYPESENSE_PROTOCOL")),
+                }
+            ],
+            "connection_timeout_seconds": 60 * 60,
+        })
+
+    def upsert(self, document):
+        return self.client.collections[os.getenv("TYPESENSE_COLLECTION_NAME")].documents.upsert(document)
+
+    def delete(self, id: str):
+        return self.client.collections[os.getenv("TYPESENSE_COLLECTION_NAME")].documents[id].delete()
\ No newline at end of file
-- 
GitLab