From 3056b8c3603611e6e0fb009ce40bbb0112c49a77 Mon Sep 17 00:00:00 2001
From: Rodrigo Goncalves <rodrigo.g@ufsc.br>
Date: Thu, 29 Aug 2024 18:48:59 +0000
Subject: [PATCH] Sample clientes and usage for SmartDataContext in PHP and
 Python

---
 sample-clients/.gitignore       |   3 +
 sample-clients/php/client.php   | 275 ++++++++++++++++++++++++++++++++
 sample-clients/php/main.php     | 151 ++++++++++++++++++
 sample-clients/python/client.py | 199 +++++++++++++++++++++++
 sample-clients/python/main.py   | 118 ++++++++++++++
 5 files changed, 746 insertions(+)
 create mode 100644 sample-clients/.gitignore
 create mode 100644 sample-clients/php/client.php
 create mode 100644 sample-clients/php/main.php
 create mode 100644 sample-clients/python/client.py
 create mode 100644 sample-clients/python/main.py

diff --git a/sample-clients/.gitignore b/sample-clients/.gitignore
new file mode 100644
index 0000000..a4cf1f5
--- /dev/null
+++ b/sample-clients/.gitignore
@@ -0,0 +1,3 @@
+*.key
+*.pem
+/.idea
\ No newline at end of file
diff --git a/sample-clients/php/client.php b/sample-clients/php/client.php
new file mode 100644
index 0000000..c568ebe
--- /dev/null
+++ b/sample-clients/php/client.php
@@ -0,0 +1,275 @@
+<?php
+
+namespace SmartDataContextAPI;
+
+class SmartDataContextAPIClient
+{
+    private string $url;
+    private string $certFile;
+    private string $keyFile;
+    private bool $verifyCertificate;
+
+    public function __construct(string $certFile, string $keyFile, string $url = "https://iot.lisha.ufsc.br/", bool $verifyCertificate = true)
+    {
+        $this->certFile = $certFile;
+        $this->keyFile = $keyFile;
+        $this->url = rtrim($url, '/') . '/';
+        $this->verifyCertificate = $verifyCertificate;
+    }
+
+    private function doJsonPost(array $json_data, string $return_attribute = '', string $endpoint = "api/v1_1/context.php")
+    {
+        $headers = [
+            'Content-Type: application/json'
+        ];
+        $url = $this->url . $endpoint;
+        $response = $this->makeRequest($url, $headers, json_encode($json_data));
+
+        $response_json = json_decode($response, true);
+        if (isset($response_json['errors'])) {
+            throw new Exception("Error processing request: " . json_encode($response_json['errors']));
+        }
+
+        if ($return_attribute) {
+            return $response_json['result'][$return_attribute];
+        }
+
+        return $response_json['result'];
+    }
+
+    private function makeRequest(string $url, array $headers, $data = null)
+    {
+        $ch = curl_init($url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSLCERT, $this->certFile);
+        curl_setopt($ch, CURLOPT_SSLKEY, $this->keyFile);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_POST, true);
+
+        if (!$this->verifyCertificate) {
+            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        }
+
+        if ($data) {
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+        }
+
+        $response = curl_exec($ch);
+        if (curl_errno($ch)) {
+            throw new Exception('Curl error: ' . curl_error($ch));
+        }
+
+        curl_close($ch);
+
+        return $response;
+    }
+
+    public function createSmartDataContext($content, $features, $t0 = -1, $t1 = -1, $smartDataSources = [], $smartDataUnits = [])
+    {
+        $json_request = [
+            "command" => "/create",
+            "request" => [
+                "content" => $content,
+                "features" => $features,
+                "t0" => $t0,
+                "t1" => $t1,
+                "smartDataUnits" => $smartDataUnits,
+                "smartDataSources" => $smartDataSources
+            ]
+        ];
+
+        return $this->doJsonPost($json_request, 'smartDataContextId');
+    }
+
+    public function associateSmartDataContext($smartDataContextIds, $smartDataUnits = [], $smartDataSources = [])
+    {
+        if (empty($smartDataSources) && empty($smartDataUnits)) {
+            throw new Exception("At least one smartDataSource or smartDataUnit must be informed");
+        }
+
+        $json_request = [
+            "command" => "/associate",
+            "request" => [
+                "smartDataContextIds" => $smartDataContextIds,
+                "smartDataUnits" => $smartDataUnits,
+                "smartDataSources" => $smartDataSources
+            ]
+        ];
+
+        $result = $this->doJsonPost($json_request);
+
+        return count($result) === 1 ? $result[0] : $result;
+    }
+
+    public function unassociateSmartDataContext($smartDataContextIds, $smartDataUnits = [], $smartDataSources = [])
+    {
+        if (empty($smartDataSources) && empty($smartDataUnits)) {
+            throw new Exception("At least one smartDataSource or smartDataUnit must be informed");
+        }
+
+        $json_request = [
+            "command" => "/unassociate",
+            "request" => [
+                "smartDataContextIds" => $smartDataContextIds,
+                "smartDataUnits" => $smartDataUnits,
+                "smartDataSources" => $smartDataSources
+            ]
+        ];
+
+        $result = $this->doJsonPost($json_request);
+
+        return count($result) === 1 ? $result[0] : $result;
+    }
+
+    public function getSmartDataContext($smartDataContextId)
+    {
+        $json_request = [
+            "command" => "/get",
+            "request" => [
+                "smartDataContextId" => $smartDataContextId
+            ]
+        ];
+
+        return $this->doJsonPost($json_request);
+    }
+
+    public function findSmartDataContext($smartDataUnits = [], $smartDataSources = [], $t0 = null, $t1 = null)
+    {
+        if (empty($smartDataSources) && empty($smartDataUnits)) {
+            throw new Exception("At least one smartDataSource or smartDataUnit must be informed");
+        }
+
+        $json_request = [
+            "command" => "/contexts",
+            "request" => [
+                "smartDataUnits" => $smartDataUnits,
+                "smartDataSources" => $smartDataSources
+            ]
+        ];
+
+        if ($t0) {
+            $json_request['request']['t0'] = $t0;
+        }
+
+        if ($t1) {
+            $json_request['request']['t1'] = $t1;
+        }
+
+        return $this->doJsonPost($json_request);
+    }
+
+    public function querySmartDataContext($query)
+    {
+        $json_request = [
+            "command" => "/query",
+            "request" => $query
+        ];
+
+        return $this->doJsonPost($json_request);
+    }
+
+    public function updateSmartDataContext($id, $content = null, $features = null, $t0 = null, $t1 = null, $smartDataSources = null, $smartDataUnits = null)
+    {
+        $json_request = [
+            "command" => "/update",
+            "request" => [
+                "smartDataContextId" => $id
+            ]
+        ];
+
+        if ($content) {
+            $json_request['request']['content'] = $content;
+        }
+
+        if ($features) {
+            $json_request['request']['features'] = $features;
+        }
+
+        if ($t0) {
+            $json_request['request']['t0'] = $t0;
+        }
+
+        if ($t1) {
+            $json_request['request']['t1'] = $t1;
+        }
+
+        if ($smartDataSources) {
+            $json_request['request']['smartDataSources'] = $smartDataSources;
+        }
+
+        if ($smartDataUnits) {
+            $json_request['request']['smartDataUnits'] = $smartDataUnits;
+        }
+
+        return $this->doJsonPost($json_request);
+    }
+
+    public function addUnstructuredDataFromFile($smartDataContextId, $filePath, $fileName = null, $mimeType = null)
+    {
+        if (!$fileName) {
+            $fileName = basename($filePath);
+        }
+
+        if (!$mimeType) {
+            $mimeType = mime_content_type($filePath);
+        }
+
+        $data = file_get_contents($filePath);
+
+        return $this->addUnstructuredData($smartDataContextId, $fileName, $mimeType, $data);
+    }
+
+    public function addUnstructuredData($smartDataContextId, $fileName, $mimeType, $data)
+    {
+        $headers = [
+            "Content-Type: $mimeType",
+            "Filename: $fileName"
+        ];
+        $url = $this->url . "api/v1_1/context.php?action=add-unstructured&smartDataContextId=$smartDataContextId";
+
+        $response = $this->makeRequest($url, $headers, $data);
+
+        $response_json = json_decode($response, true);
+
+        if (isset($response_json['errors'])) {
+            throw new Exception("Error processing request: " . json_encode($response_json['errors']));
+        }
+
+        return $response_json['result']['objectId'];
+    }
+
+    public function getUnstructuredData($smartDataContextId, $objectId)
+    {
+        $url = $this->url . "api/v1_1/context.php";
+        $json_request = [
+            "command" => "/unstructured/get",
+            "request" => [
+                "smartDataContextId" => $smartDataContextId,
+                "objectId" => $objectId
+            ]
+        ];
+
+        return $this->makeRequest($url, [], json_encode($json_request));
+    }
+
+    public function saveUnstructuredDataToFile($smartDataContextId, $objectId, $filePath)
+    {
+        $data = $this->getUnstructuredData($smartDataContextId, $objectId);
+        file_put_contents($filePath, $data);
+    }
+
+    public function removeUnstructuredData($smartDataContextId, $objectId)
+    {
+        $json_request = [
+            "command" => "/unstructured/remove",
+            "request" => [
+                "smartDataContextId" => $smartDataContextId,
+                "objectId" => $objectId
+            ]
+        ];
+
+        return $this->doJsonPost($json_request);
+    }
+}
+?>
diff --git a/sample-clients/php/main.php b/sample-clients/php/main.php
new file mode 100644
index 0000000..b342104
--- /dev/null
+++ b/sample-clients/php/main.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+
+require_once 'client.php';
+
+use SmartDataContextAPI\SmartDataContextAPIClient;
+
+const CLIENT_CERTIFICATE = 'client.pem';
+const CLIENT_CERTIFICATE_KEY = 'client.key';
+const API_URL = 'https://localhost/';
+
+function generateRandomFile(string $filename, int $sizeInMb): void
+{
+    $sizeInBytes = $sizeInMb * 1024 * 1024;
+    $data = random_bytes($sizeInBytes);
+    file_put_contents($filename, $data);
+}
+
+# Create a new SmartDataContextClient, passing a custom URL for the API and the certificates to access the domain
+# The parameter verifyCertificate allows using self-signed certificates for development and test environments.
+# Should not be used in production ideally
+$client = new SmartDataContextAPIClient(
+    certFile: CLIENT_CERTIFICATE,
+    keyFile: CLIENT_CERTIFICATE_KEY,
+    url: API_URL,
+    verifyCertificate: false
+);
+
+# Creates a new perene SmartDataContext (no start not end time). The minimum required parameters are the content
+# and the features, with at least one feature (tags), which indicate tags to be associated with this SmartDataContext
+# The function returns the create SmartDataContext id or throws an exception in case of error
+$pereneSmartDataContextId = $client->createSmartDataContext(
+    content: ['meta' => true],
+    features: ['tags' => ['sampleTag', 'simulation']]
+);
+echo "Created a new SmartDataContext with id {$pereneSmartDataContextId}\n";
+
+# Now that we have a smartDataContext we can associate it to a SmartDataUnit or to SmartDataSource
+# A smartDataUnit is represented by its value
+# A smartDataSource is represented by its sphere (stationary) or its signature (mobile)
+# Either smartDataUnits or smartDataSources or both can be associated to a smartDataContext
+# A list of smartDataContextIds could be passed as well
+# The function returns the list of updated SmartDataContext object or the single SmartDataContext
+$smartDataUnits = [0x84963924];
+$smartDataSources = [[1, 1, 2, 10], 'aeccd287'];
+$pereneSmartDataContext = $client->associateSmartDataContext(
+    smartDataUnits: $smartDataUnits,
+    smartDataSources: $smartDataSources,
+    smartDataContextIds: $pereneSmartDataContextId
+);
+echo "Updated SmartDataContext: \n" . json_encode($pereneSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+# To unassociate just call the unassociateSmartDataContext following the same syntax
+$smartDataSources = [[1, 1, 2, 10]];
+$pereneSmartDataContext = $client->unassociateSmartDataContext(
+    smartDataSources: $smartDataSources,
+    smartDataContextIds: $pereneSmartDataContextId
+);
+echo "Updated SmartDataContext: \n" . json_encode($pereneSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+# To retrieve a SmartDataContext by its id
+$pereneSmartDataContext = $client->getSmartDataContext(smartDataContextId: $pereneSmartDataContextId);
+echo "Retrieved SmartDataContext: \n" . json_encode($pereneSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+# To find all SmartDataContext associated to a set of smartDataSources or smartDataUnits
+$smartDataContextForSources = $client->findSmartDataContext(smartDataSources: $smartDataSources);
+echo "Retrieved SmartDataContext for sources: " . count($smartDataContextForSources) . "\n" . json_encode($smartDataContextForSources, JSON_PRETTY_PRINT) . "\n";
+
+$smartDataContextForUnits = $client->findSmartDataContext(smartDataUnits: $smartDataUnits);
+echo "Retrieved SmartDataContext for units: " . count($smartDataContextForUnits) . "\n" . json_encode($smartDataContextForUnits, JSON_PRETTY_PRINT) . "\n";
+
+$smartDataContextForUnitsOrSources = $client->findSmartDataContext(smartDataUnits: $smartDataUnits, smartDataSources: $smartDataSources);
+echo "Retrieved SmartDataContext for sources and units: " . count($smartDataContextForUnitsOrSources) . "\n" . json_encode($smartDataContextForUnitsOrSources, JSON_PRETTY_PRINT) . "\n";
+
+# To find all SmartDataContext associated to a set of smartDataSources or smartDataUnits under a time range
+$temporalSmartDataContextId = $client->createSmartDataContext(
+    content: ['meta' => true],
+    features: ['tags' => ['sampleTag', 'simulation']],
+    smartDataUnits: $smartDataUnits,
+    t0: 15,
+    t1: 25
+);
+$temporalSmartDataContext = $client->getSmartDataContext(smartDataContextId: $temporalSmartDataContextId);
+echo "Retrieved temporal SmartDataContext: \n" . json_encode($temporalSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+$smartDataContextForUnitsOrSources = $client->findSmartDataContext(
+    smartDataUnits: $smartDataUnits,
+    smartDataSources: $smartDataSources,
+    t0: 16,
+    t1: 20
+);
+echo "Retrieved SmartDataContext between 16 and 20: " . count($smartDataContextForUnitsOrSources) . "\n" . json_encode($smartDataContextForUnitsOrSources, JSON_PRETTY_PRINT) . "\n";
+
+# To find all SmartDataContext associated to a set of smartDataSources or smartDataUnits under a time range
+$smartDataContextForUnitsOrSources = $client->findSmartDataContext(
+    smartDataUnits: $smartDataUnits,
+    smartDataSources: $smartDataSources,
+    t0: 5,
+    t1: 9
+);
+echo "Retrieved SmartDataContext between 5 and 9: " . count($smartDataContextForUnitsOrSources) . "\n" . json_encode($smartDataContextForUnitsOrSources, JSON_PRETTY_PRINT) . "\n";
+
+# To do a generic query using the MongoDB syntax
+$query = ['t0' => 15];
+$queryResult = $client->querySmartDataContext(query: $query);
+echo "Query result: \n" . json_encode($queryResult, JSON_PRETTY_PRINT) . "\n";
+
+# To change the contents of a SmartDataContext
+$temporalSmartDataContext = $client->updateSmartDataContext($temporalSmartDataContextId, content: ['meta' => false]);
+echo "Updated temporal SmartDataContext: \n" . json_encode($temporalSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+# To add an unstructured content to a SmartDataContext
+generateRandomFile('data.bin', 3);
+$storageObjectId = $client->addUnstructuredDataFromFile($temporalSmartDataContextId, 'data.bin');
+echo "Stored object id is {$storageObjectId}\n";
+$temporalSmartDataContext = $client->getSmartDataContext($temporalSmartDataContextId);
+echo "Updated temporal SmartDataContext: \n" . json_encode($temporalSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+generateRandomFile('data2.bin', 3);
+$storageObjectId2 = $client->addUnstructuredDataFromFile($temporalSmartDataContextId, 'data2.bin');
+echo "Stored object id is {$storageObjectId2}\n";
+$temporalSmartDataContext = $client->getSmartDataContext($temporalSmartDataContextId);
+echo "Updated temporal SmartDataContext: \n" . json_encode($temporalSmartDataContext, JSON_PRETTY_PRINT) . "\n";
+
+# To retrieve an unstructured content from a SmartDataContent
+$client->saveUnstructuredDataToFile($temporalSmartDataContextId, $storageObjectId, 'data_retrieved.bin');
+if (hash_file('md5', 'data.bin') === hash_file('md5', 'data_retrieved.bin')) {
+    echo "Retrieved file data.bin is identical\n";
+} else {
+    throw new Exception("Retrieved file data.bin is not identical");
+}
+
+$client->saveUnstructuredDataToFile($temporalSmartDataContextId, $storageObjectId2, 'data2_retrieved.bin');
+if (hash_file('md5', 'data2.bin') === hash_file('md5', 'data2_retrieved.bin')) {
+    echo "Retrieved file data2.bin is identical\n";
+} else {
+    throw new Exception("Retrieved file data2.bin is not identical");
+}
+
+unlink('data_retrieved.bin');
+unlink('data2_retrieved.bin');
+unlink('data.bin');
+unlink('data2.bin');
+
+# To remove a unstructured data
+$temporalSmartDataContext = $client->removeUnstructuredData($temporalSmartDataContextId, $storageObjectId);
+echo "Removed unstructured data with id {$storageObjectId}. Unstructured count: " . count($temporalSmartDataContext['unstructuredData']) . " with id " . $temporalSmartDataContext['unstructuredData'][0]['id'] . "\n";
+
+$temporalSmartDataContext = $client->removeUnstructuredData($temporalSmartDataContextId, $storageObjectId2);
+echo "Removed unstructured data with id {$storageObjectId2}. Unstructured count: " . count($temporalSmartDataContext['unstructuredData']) . "\n";
diff --git a/sample-clients/python/client.py b/sample-clients/python/client.py
new file mode 100644
index 0000000..df3ce60
--- /dev/null
+++ b/sample-clients/python/client.py
@@ -0,0 +1,199 @@
+import mimetypes
+import os
+
+import requests
+import json
+
+class SmartDataContextAPIClient:
+    def __init__(self, cert_file, key_file, url="https://iot.lisha.ufsc.br/", verifyCertificate=True):
+        if not url.endswith('/'):
+            url += '/'
+        self._url = url
+        self._client = requests.Session()
+        self._client.cert = (cert_file, key_file)
+        self._client.verify = verifyCertificate
+        if not verifyCertificate:
+            requests.urllib3.disable_warnings()
+
+    def _doJsonPost(self, json_data, return_attribute = '', endpoint="api/v1_1/context.php"):
+        headers = {
+            'Content-Type': 'application/json'
+        }
+        url = self._url + endpoint
+        response = self._client.post(url, headers=headers, json=json_data)
+        try:
+            response_json = response.json()
+            if "errors" in response_json:
+                raise Exception(f"Error processing request: {json.dumps(response_json['errors'])}")
+            else:
+                if return_attribute:
+                    return response_json['result'][return_attribute]
+                else:
+                    return response_json['result']
+        except Exception as e:
+            raise Exception(f"Invalid response from API: {response.content} - {e}")
+
+    def createSmartDataContext(self, content, features, t0=-1, t1=-1, smartDataSources=[], smartDataUnits=[]):
+        json_request = {
+            "command": "/create",
+            "request": {
+                "content": content,
+                "features": features,
+                "t0": t0,
+                "t1": t1,
+                "smartDataUnits": smartDataUnits,
+                "smartDataSouces": smartDataSources
+            }
+        }
+        return self._doJsonPost(json_request, 'smartDataContextId')
+
+
+    def associateSmartDataContext(self, smartDataContextIds, smartDataUnits=[], smartDataSources=[]):
+        if len(smartDataSources) == 0 and len(smartDataUnits) == 0:
+            raise Exception("At least one smartDataSource or smartDataUnit must be informed")
+        json_request = {
+            "command": "/associate",
+            "request": {
+                "smartDataContextIds": smartDataContextIds,
+                "smartDataUnits": smartDataUnits,
+                "smartDataSources": smartDataSources
+            }
+        }
+
+        result = self._doJsonPost(json_request)
+        if len(result) == 1:
+            return result[0]
+        else:
+            return result
+
+    def unassociateSmartDataContext(self, smartDataContextIds, smartDataUnits=[], smartDataSources=[]):
+        if len(smartDataSources) == 0 and len(smartDataUnits) == 0:
+            raise Exception("At least one smartDataSource or smartDataUnit must be informed")
+        json_request = {
+            "command": "/unassociate",
+            "request": {
+                "smartDataContextIds": smartDataContextIds,
+                "smartDataUnits": smartDataUnits,
+                "smartDataSources": smartDataSources
+            }
+        }
+
+        result = self._doJsonPost(json_request)
+        if len(result) == 1:
+            return result[0]
+        else:
+            return result
+
+    def getSmartDataContext(self, smartDataContextId):
+        json_request = {
+            "command": "/get",
+            "request": {
+                "smartDataContextId": smartDataContextId
+            }
+        }
+
+        return self._doJsonPost(json_request)
+
+    def findSmartDataContext(self, smartDataUnits=[], smartDataSources=[], t0=None, t1=None):
+        if len(smartDataSources) == 0 and len(smartDataUnits) == 0:
+            raise Exception("At least one smartDataSource or smartDataUnit must be informed")
+        json_request = {
+            "command": "/contexts",
+            "request": {
+                "smartDataUnits": smartDataUnits,
+                "smartDataSources": smartDataSources
+            }
+        }
+        if t0:
+            json_request['request']['t0'] = t0
+        if t1:
+            json_request['request']['t1'] = t1
+
+        return self._doJsonPost(json_request)
+
+    def querySmartDataContext(self, query):
+        json_request = {
+            "command": "/query",
+            "request": query
+        }
+        return self._doJsonPost(json_request)
+
+    def updateSmartDataContext(self, id, content=None, features=None, t0=None, t1=None, smartDataSources=None, smartDataUnits=None):
+        json_request = {
+            "command": "/update",
+            "request": {
+                "smartDataContextId": id
+            }
+        }
+        if content:
+            json_request['request']['content'] = content
+        if features:
+            json_request['request']['features'] = features
+        if t0:
+            json_request['request']['t0'] = t0
+        if t1:
+            json_request['request']['t1'] = t1
+        if smartDataSources:
+            json_request['request']['smartDataSources'] = smartDataSources
+        if smartDataUnits:
+            json_request['request']['smartDataUnits'] = smartDataUnits
+
+        return self._doJsonPost(json_request)
+
+    def addUnstructuredDataFromFile(self, smartDataContextId, filePath, fileName=None, mimeType=None):
+        if not fileName:
+            fileName = os.path.basename(filePath)
+        if not mimeType:
+            mimeType, _ = mimetypes.guess_type(filePath)
+        with open(filePath, 'rb') as f:
+            return self.addUnstructuredData(smartDataContextId, fileName, mimeType, f)
+
+    def addUnstructuredData(self, smartDataContextId, fileName, mimeType, data):
+        headers = {
+            'Content-Type': mimeType,
+            'Filename': fileName
+        }
+        url = self._url + f"api/v1_1/context.php?action=add-unstructured&smartDataContextId={smartDataContextId}"
+        response = self._client.post(url, headers=headers, data=data)
+        try:
+            response_json = response.json()
+            if "errors" in response_json:
+                raise Exception(f"Error processing request: {json.dumps(response_json['errors'])}")
+            else:
+                return response_json['result']['objectId']
+        except Exception as e:
+            raise Exception(f"Invalid response from API: {response.content} - {e}")
+
+    def getUnstructuredData(self, smartDataContextId, objectId):
+        headers = {
+        }
+        url = self._url + "api/v1_1/context.php"
+        json_request = {
+            "command": "/unstructured/get",
+            "request": {
+                "smartDataContextId": smartDataContextId,
+                "objectId": objectId
+            }
+        }
+        response = self._client.post(url, headers=headers, json=json_request)
+        try:
+            response.raise_for_status()
+            return response.content
+        except Exception as e:
+            raise Exception(f"Invalid response from API: {e}")
+
+    def saveUnstructuredDataToFile(self, smartDataContextId, objectId, filePath):
+        data = self.getUnstructuredData(smartDataContextId, objectId)
+        with open (filePath, "wb") as f:
+            f.write(data)
+
+    def removeUnstructuredData(self, smartDataContextId, objectId):
+        json_request = {
+            "command": "/unstructured/remove",
+            "request": {
+                "smartDataContextId": smartDataContextId,
+                "objectId": objectId
+            }
+        }
+
+        return self._doJsonPost(json_request)
\ No newline at end of file
diff --git a/sample-clients/python/main.py b/sample-clients/python/main.py
new file mode 100644
index 0000000..e4b7298
--- /dev/null
+++ b/sample-clients/python/main.py
@@ -0,0 +1,118 @@
+import filecmp
+import json
+import os
+
+from client import SmartDataContextAPIClient
+
+CLIENT_CERTIFICATE="client.pem"
+CLIENT_CERTIFICATE_KEY="client.key"
+API_URL="https://localhost/"
+
+def generate_random_file(filename, size_in_mb):
+    size_in_bytes = size_in_mb * 1024 * 1024
+    with open(filename, 'wb') as f:
+        f.write(os.urandom(size_in_bytes))
+
+# Create a new SmartDataContextClient, passing a custom URL for the API and the certificates to access the domain
+# The parameter verifyCertificate allows using self-signed certificates for development and test environments.
+# Should not be used in production ideally
+client = SmartDataContextAPIClient(cert_file=CLIENT_CERTIFICATE, key_file=CLIENT_CERTIFICATE_KEY, url=API_URL, verifyCertificate=False)
+
+# Creates a new perene SmartDataContext (no start not end time). The minimum required parameters are the content
+# and the features, with at least one feature (tags), which indicate tags to be associated with this SmartDataContext
+# The function returns the create SmartDataContext id or throws an exception in case of error
+pereneSmartDataContextId = client.createSmartDataContext(content={"meta": True}, features={"tags": ["sampleTag", "simulation"]})
+print(f"Create a new SmartDataContext with id {pereneSmartDataContextId}")
+
+# Now that we have a smartDataContext we can associate it to a SmartDataUnit or to SmartDataSource
+# A smartDataUnit is represented by its value
+# A smartDataSource is represented by its sphere (stationary) or its signature (mobile)
+# Either smartDataUnits or smartDataSources or both can be associated to a smartDataContext
+# A list of smartDataContextIds could be passed as well
+# The function returns the list of updated SmartDataContext object or the single SmartDataContext
+smartDataUnits = [0x84963924]
+smartDataSources = [ [1,1,2,10], 'aeccd287']
+pereneSmartDataContext = client.associateSmartDataContext(smartDataUnits=smartDataUnits, smartDataSources=smartDataSources, smartDataContextIds=pereneSmartDataContextId)
+print(f"Updated SmartDataContext \n {json.dumps(pereneSmartDataContext, indent=2)}")
+
+# To unassociate just call the unassociateSmartDataContext following the same syntax
+smartDataSources = [ [1,1,2,10] ]
+pereneSmartDataContext = client.unassociateSmartDataContext(smartDataSources=smartDataSources, smartDataContextIds=pereneSmartDataContextId)
+print(f"Updated SmartDataContext \n {json.dumps(pereneSmartDataContext, indent=2)}")
+
+# To retrieve a SmartDataContext by its id
+pereneSmartDataContext = client.getSmartDataContext(smartDataContextId=pereneSmartDataContextId)
+print(f"Retrieved SmartDataContext \n {json.dumps(pereneSmartDataContext, indent=2)}")
+
+# To find all SmartDataContext associated to a set of smartDataSources or smartDataUnits
+smartDataContextForSources = client.findSmartDataContext(smartDataSources=smartDataSources)
+print(f"Retrieved SmartDataContext for {smartDataSources}: {len(smartDataContextForSources)} \n {json.dumps(smartDataContextForSources, indent=2)}")
+
+smartDataContextForUnits = client.findSmartDataContext(smartDataUnits=smartDataUnits)
+print(f"Retrieved SmartDataContext for {smartDataSources}: {len(smartDataContextForUnits)} \n {json.dumps(smartDataContextForUnits, indent=2)}")
+
+smartDataContextForUnitsOrSources = client.findSmartDataContext(smartDataUnits=smartDataUnits, smartDataSources=smartDataSources)
+print(f"Retrieved SmartDataContext for {smartDataSources} and {smartDataUnits}: {len(smartDataContextForUnitsOrSources)} \n {json.dumps(smartDataContextForUnitsOrSources, indent=2)}")
+
+# To find all SmartDataContext associated to a set of smartDataSources or smartDataUnits under a time range
+temporalSmartDataContextId = client.createSmartDataContext(content={"meta": True}, features={"tags": ["sampleTag", "simulation"]},
+                                                         smartDataUnits=smartDataUnits, t0=15, t1=25)
+temporalSmartDataContext = client.getSmartDataContext(smartDataContextId=temporalSmartDataContextId)
+print(f"Retrieved temporal SmartDataContext \n {json.dumps(temporalSmartDataContext, indent=2)}")
+
+
+smartDataContextForUnitsOrSources = client.findSmartDataContext(smartDataUnits=smartDataUnits, smartDataSources=smartDataSources, t0=16, t1=20)
+print(f"Retrieved SmartDataContext for {smartDataSources} and {smartDataUnits} between 16 and 20 : {len(smartDataContextForUnitsOrSources)} \n {json.dumps(smartDataContextForUnitsOrSources, indent=2)}")
+
+
+# To find all SmartDataContext associated to a set of smartDataSources or smartDataUnits under a time range
+smartDataContextForUnitsOrSources = client.findSmartDataContext(smartDataUnits=smartDataUnits, smartDataSources=smartDataSources, t0=5, t1=9)
+print(f"Retrieved SmartDataContext for {smartDataSources} and {smartDataUnits} between 5 and 9 : {len(smartDataContextForUnitsOrSources)} \n {json.dumps(smartDataContextForUnitsOrSources, indent=2)}")
+
+# To do a generic query using the MongoDB syntax
+query = {
+    "t0": 15
+}
+queryResult = client.querySmartDataContext(query)
+print(f"Query result: \n {json.dumps(queryResult, indent=2)}")
+
+# To change the contents of a SmartDataContext
+temporalSmartDataContext = client.updateSmartDataContext(temporalSmartDataContextId, content={"meta": False})
+print(f"Updated temporal SmartDataContext \n {json.dumps(temporalSmartDataContext, indent=2)}")
+
+# To add an unstructured content to a SmartDataContext
+generate_random_file("data.bin", 3)
+storageObjectId = client.addUnstructuredDataFromFile(temporalSmartDataContextId, 'data.bin');
+print(f"Stored object id is {storageObjectId}")
+temporalSmartDataContext = client.getSmartDataContext(temporalSmartDataContextId);
+print(f"Updated temporal SmartDataContext \n {json.dumps(temporalSmartDataContext, indent=2)}")
+
+generate_random_file("data2.bin", 3)
+storageObjectId2 = client.addUnstructuredDataFromFile(temporalSmartDataContextId, 'data2.bin');
+print(f"Stored object id is {storageObjectId}")
+temporalSmartDataContext = client.getSmartDataContext(temporalSmartDataContextId);
+print(f"Updated temporal SmartDataContext \n {json.dumps(temporalSmartDataContext, indent=2)}")
+
+# To retrieve an unstructured content from a SmartDataContent
+client.saveUnstructuredDataToFile(temporalSmartDataContextId, storageObjectId, "data_retrieved.bin");
+if filecmp.cmp("data.bin", "data_retrieved.bin"):
+    print("Retrieved file data.bin is identical\n")
+else:
+    raise Exception("Retrieved file data.bin is not identical")
+client.saveUnstructuredDataToFile(temporalSmartDataContextId, storageObjectId2, "data2_retrieved.bin");
+if filecmp.cmp("data2.bin", "data2_retrieved.bin"):
+    print("Retrieved file data2.bin is identical\n")
+else:
+    raise Exception("Retrieved file data2.bin is not identical")
+
+os.remove("data_retrieved.bin")
+os.remove("data2_retrieved.bin")
+os.remove("data.bin")
+os.remove("data2.bin")
+
+# To remove a unstructured data
+temporalSmartDataContext = client.removeUnstructuredData(temporalSmartDataContextId, storageObjectId)
+print(f"Removed unstructured {storageObjectId} - containing unstructured count is {len(temporalSmartDataContext['unstructuredData'])} with id {temporalSmartDataContext['unstructuredData'][0]['id']}")
+
+temporalSmartDataContext = client.removeUnstructuredData(temporalSmartDataContextId, storageObjectId2)
+print(f"Removed unstructured {storageObjectId2} - containing unstructured count is {len(temporalSmartDataContext['unstructuredData'])}")
-- 
GitLab