From 0fff43ed25de51d0f9d67d99094bb39443a66412 Mon Sep 17 00:00:00 2001
From: Martin Weise <martin.weise@tuwien.ac.at>
Date: Wed, 10 Apr 2024 11:47:06 +0200
Subject: [PATCH] Added documentation

---
 lib/python/Makefile                     | 26 +++++++++
 lib/python/README.md                    | 74 +++++++++++++++----------
 lib/python/dbrepo/AmqpClient.py         | 16 +++---
 lib/python/dbrepo/RestClient.py         | 23 ++++----
 lib/python/dbrepo/UploadClient.py       | 39 +++++++++++++
 lib/python/docs/guide/amqp-client.rst   |  7 +--
 lib/python/docs/guide/rest-client.rst   |  7 +--
 lib/python/docs/guide/upload-client.rst |  6 ++
 lib/python/docs/index.rst               | 36 +++++++-----
 9 files changed, 166 insertions(+), 68 deletions(-)
 create mode 100644 lib/python/Makefile
 create mode 100644 lib/python/dbrepo/UploadClient.py
 create mode 100644 lib/python/docs/guide/upload-client.rst

diff --git a/lib/python/Makefile b/lib/python/Makefile
new file mode 100644
index 0000000000..4b9e18e3ad
--- /dev/null
+++ b/lib/python/Makefile
@@ -0,0 +1,26 @@
+all:
+
+clean:
+	rm -rf ./python/dist/* ./docs/build/* ./dist/*
+
+install:
+	pipenv install
+
+docs: clean
+	sphinx-apidoc -o ./docs/source ./dbrepo
+	sphinx-build -M html ./docs/ ./docs/build/
+
+check:
+	python3 ./python/setup.py develop
+
+build: clean
+	python3 -m build --sdist .
+	python3 -m build --wheel .
+
+deploy: build
+	python3 -m twine upload --config-file ~/.pypirc --verbose --repository pypi ./dist/dbrepo-*
+
+deploy-test: build
+	python3 -m twine upload --config-file ~/.pypirc --verbose --repository testpypi ./dist/dbrepo-*
+
+FORCE: ;
\ No newline at end of file
diff --git a/lib/python/README.md b/lib/python/README.md
index 0cb742a451..610c6eded4 100644
--- a/lib/python/README.md
+++ b/lib/python/README.md
@@ -32,33 +32,33 @@ print(f"Analysis result: {analysis}")
 
 # create table
 table = client.create_table(database_id=1,
-    name="Sensor Data",
-    constraints=CreateTableConstraints(
-        checks=['precipitation >= 0'],
-        uniques=[['precipitation']]),
-    columns=[CreateTableColumn(name="date",
-                               type=ColumnType.DATE,
-                               dfid=3,  # YYYY-MM-dd
-                               primary_key=True,
-                               null_allowed=False),
-             CreateTableColumn(name="precipitation",
-                               type=ColumnType.DECIMAL,
-                               size=10,
-                               d=4,
-                               primary_key=False,
-                               null_allowed=True),
-             CreateTableColumn(name="lat",
-                               type=ColumnType.DECIMAL,
-                               size=10,
-                               d=4,
-                               primary_key=False,
-                               null_allowed=True),
-             CreateTableColumn(name="lng",
-                               type=ColumnType.DECIMAL,
-                               size=10,
-                               d=4,
-                               primary_key=False,
-                               null_allowed=True)])
+                            name="Sensor Data",
+                            constraints=CreateTableConstraints(
+                                checks=['precipitation >= 0'],
+                                uniques=[['precipitation']]),
+                            columns=[CreateTableColumn(name="date",
+                                                       type=ColumnType.DATE,
+                                                       dfid=3,  # YYYY-MM-dd
+                                                       primary_key=True,
+                                                       null_allowed=False),
+                                     CreateTableColumn(name="precipitation",
+                                                       type=ColumnType.DECIMAL,
+                                                       size=10,
+                                                       d=4,
+                                                       primary_key=False,
+                                                       null_allowed=True),
+                                     CreateTableColumn(name="lat",
+                                                       type=ColumnType.DECIMAL,
+                                                       size=10,
+                                                       d=4,
+                                                       primary_key=False,
+                                                       null_allowed=True),
+                                     CreateTableColumn(name="lng",
+                                                       type=ColumnType.DECIMAL,
+                                                       size=10,
+                                                       d=4,
+                                                       primary_key=False,
+                                                       null_allowed=True)])
 print(f"Create table result {table}")
 # -> (id=1, internal_name=sensor_data, ...)
 
@@ -83,7 +83,25 @@ print(f"Finished.")
   queries ([docs](https://www.ifs.tuwien.ac.at/infrastructures/dbrepo//usage-overview/#export-subset))
 - Get data from tables/views/subsets
 
-## Future
+## Configure
+
+All credentials can optionally be set/overridden with environment variables. This is especially useful when sharing 
+Jupyter Notebooks by creating an invisible `.env` file and loading it:
+
+```
+REST_API_ENDPOINT="https://test.dbrepo.tuwien.ac.at"
+REST_API_USERNAME="foo"
+REST_API_PASSWORD="bar"
+REST_API_SECURE="True"
+AMQP_API_HOST="https://test.dbrepo.tuwien.ac.at"
+AMQP_API_PORT="5672"
+AMQP_API_USERNAME="foo"
+AMQP_API_PASSWORD="bar"
+AMQP_API_VIRTUAL_HOST="/"
+REST_UPLOAD_ENDPOINT="https://test.dbrepo.tuwien.ac.at/api/upload/files"
+```
+
+## Roadmap
 
 - Searching
 
diff --git a/lib/python/dbrepo/AmqpClient.py b/lib/python/dbrepo/AmqpClient.py
index c9fcdc05ff..29f7e261ec 100644
--- a/lib/python/dbrepo/AmqpClient.py
+++ b/lib/python/dbrepo/AmqpClient.py
@@ -2,8 +2,8 @@ import dataclasses
 import os
 import pika
 import sys
-import logging
 import json
+import logging
 
 from dbrepo.api.dto import CreateData
 
@@ -32,16 +32,18 @@ class AmqpClient:
                  broker_virtual_host: str = '/',
                  username: str = None,
                  password: str = None) -> None:
+        logging.getLogger('requests').setLevel(logging.INFO)
+        logging.getLogger('urllib3').setLevel(logging.INFO)
         logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-6s %(message)s', level=logging.DEBUG,
                             stream=sys.stdout)
-        self.broker_host = os.environ.get('DBREPO_BROKER_HOST', broker_host)
-        self.broker_port = os.environ.get('DBREPO_BROKER_PORT', broker_port)
-        if os.environ.get('DBREPO_BROKER_VIRTUAL_HOST') is not None:
-            self.broker_virtual_host = os.environ.get('DBREPO_BROKER_VIRTUAL_HOST')
+        self.broker_host = os.environ.get('AMQP_API_HOST', broker_host)
+        self.broker_port = os.environ.get('AMQP_API_PORT', broker_port)
+        if os.environ.get('AMQP_API_VIRTUAL_HOST') is not None:
+            self.broker_virtual_host = os.environ.get('AMQP_API_VIRTUAL_HOST')
         else:
             self.broker_virtual_host = broker_virtual_host
-        self.username = os.environ.get('DBREPO_USERNAME', username)
-        self.password = os.environ.get('DBREPO_PASSWORD', password)
+        self.username = os.environ.get('AMQP_API_USERNAME', username)
+        self.password = os.environ.get('AMQP_API_PASSWORD', password)
 
     def publish(self, exchange: str, routing_key: str, data=dict) -> None:
         """
diff --git a/lib/python/dbrepo/RestClient.py b/lib/python/dbrepo/RestClient.py
index 619fec9a66..5aad7d6eb3 100644
--- a/lib/python/dbrepo/RestClient.py
+++ b/lib/python/dbrepo/RestClient.py
@@ -7,6 +7,7 @@ from pydantic import TypeAdapter
 from tusclient.client import TusClient
 from pandas import DataFrame
 
+from dbrepo.UploadClient import UploadClient
 from dbrepo.api.dto import *
 from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailExistsError, NotExistsError, \
     ForbiddenError, MalformedError, NameExistsError, QueryStoreError, MetadataConsistencyError, ExternalSystemError, \
@@ -16,8 +17,8 @@ from dbrepo.api.exceptions import ResponseCodeError, UsernameExistsError, EmailE
 class RestClient:
     """
     The RestClient class for communicating with the DBRepo REST API. All parameters can be set also via environment \
-    variables, e.g. set endpoint with DBREPO_ENDPOINT, username with DBREPO_USERNAME, etc. You can override the \
-    constructor parameters with the environment variables.
+    variables, e.g. set endpoint with REST_API_ENDPOINT, username with REST_API_USERNAME, etc. You can override \
+    the constructor parameters with the environment variables.
 
     :param endpoint: The REST API endpoint. Optional. Default: "http://gateway-service"
     :param username: The REST API username. Optional.
@@ -39,11 +40,11 @@ class RestClient:
         logging.getLogger('urllib3').setLevel(logging.INFO)
         logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-6s %(message)s', level=logging.DEBUG,
                             stream=sys.stdout)
-        self.endpoint = os.environ.get('DBREPO_ENDPOINT', endpoint)
-        self.username = os.environ.get('DBREPO_USERNAME', username)
-        self.password = os.environ.get('DBREPO_PASSWORD', password)
-        if os.environ.get('DBREPO_SECURE') is not None:
-            self.secure = os.environ.get('DBREPO_SECURE') == 'True'
+        self.endpoint = os.environ.get('REST_API_ENDPOINT', endpoint)
+        self.username = os.environ.get('REST_API_USERNAME', username)
+        self.password = os.environ.get('REST_API_PASSWORD', password)
+        if os.environ.get('REST_API_SECURE') is not None:
+            self.secure = os.environ.get('REST_API_SECURE') == 'True'
         else:
             self.secure = secure
 
@@ -87,9 +88,11 @@ class RestClient:
             raise UploadError(f'Failed to upload the file to {self.endpoint}')
         return filename
 
-    def whoami(self) -> str:
+    def whoami(self) -> str | None:
         """
         Print the username.
+
+        :returns: The username, if set.
         """
         if self.username is not None:
             logging.info(f"{self.username}")
@@ -746,8 +749,8 @@ class RestClient:
         :raises NotExistsError: If the table does not exist.
         :raises MalformedError: If the payload is rejected by the service (e.g. LOB data could not be imported).
         """
-        upload = UploadClient(endpoint=self.endpoint)
-        filename = upload.upload(file_path=file_path)
+        client = UploadClient(endpoint=self.endpoint)
+        filename = client.upload(file_path=file_path)
         url = f'/api/database/{database_id}/table/{table_id}/data/import'
         response = self._wrapper(method="post", url=url, force_auth=True,
                                  payload=Import(location=filename, separator=separator, quote=quote,
diff --git a/lib/python/dbrepo/UploadClient.py b/lib/python/dbrepo/UploadClient.py
new file mode 100644
index 0000000000..236453cb70
--- /dev/null
+++ b/lib/python/dbrepo/UploadClient.py
@@ -0,0 +1,39 @@
+import logging
+import os
+import re
+import sys
+from tusclient import client
+
+
+class UploadClient:
+    """
+    The UploadClient class for communicating with the DBRepo REST API. All parameters can be set also via environment \
+    variables, e.g. set endpoint with DBREPO_ENDPOINT, username with DBREPO_USERNAME, etc. You can override the \
+    constructor parameters with the environment variables.
+
+    :param endpoint: The REST API endpoint. Optional. Default: "http://gateway-service/api/upload/files"
+    """
+    endpoint: str = None
+
+    def __init__(self, endpoint: str = 'http://gateway-service/api/upload/files') -> None:
+        logging.getLogger('requests').setLevel(logging.INFO)
+        logging.getLogger('urllib3').setLevel(logging.INFO)
+        logging.basicConfig(format='%(asctime)s %(name)-12s %(levelname)-6s %(message)s', level=logging.DEBUG,
+                            stream=sys.stdout)
+        self.endpoint = os.environ.get('REST_UPLOAD_ENDPOINT', endpoint)
+
+    def upload(self, file_path: str) -> str:
+        """
+        Imports a file through the Upload Service into the Storage Service.
+
+        :param file_path: The file path on the local machine.
+
+        :returns: Filename on the Storage Service, if successful.
+        """
+        tus_client = client.TusClient(url=self.endpoint)
+        uploader = tus_client.uploader(file_path=file_path)
+        uploader.upload()
+        m = re.search('\\/([a-f0-9]+)\\+', uploader.url)
+        filename = m.group(0)[1:-1]
+        logging.debug(f'uploaded file {file_path} to storage service with key: {filename}')
+        return filename
diff --git a/lib/python/docs/guide/amqp-client.rst b/lib/python/docs/guide/amqp-client.rst
index 0bb970292c..7f87621f43 100644
--- a/lib/python/docs/guide/amqp-client.rst
+++ b/lib/python/docs/guide/amqp-client.rst
@@ -1,8 +1,5 @@
-AMQP Client
-===========
-
-.. warning::
-   This documentation is a work in progress.
+AMQP API Client
+===============
 
 .. automodule:: dbrepo.AmqpClient
     :members:
diff --git a/lib/python/docs/guide/rest-client.rst b/lib/python/docs/guide/rest-client.rst
index 4af546168d..09ed401943 100644
--- a/lib/python/docs/guide/rest-client.rst
+++ b/lib/python/docs/guide/rest-client.rst
@@ -1,8 +1,5 @@
-REST Client
-===========
-
-.. warning::
-   This documentation is a work in progress.
+REST API Client
+===============
 
 .. automodule:: dbrepo.RestClient
     :members:
diff --git a/lib/python/docs/guide/upload-client.rst b/lib/python/docs/guide/upload-client.rst
new file mode 100644
index 0000000000..4070b09088
--- /dev/null
+++ b/lib/python/docs/guide/upload-client.rst
@@ -0,0 +1,6 @@
+Upload API Client
+=================
+
+.. automodule:: dbrepo.UploadClient
+    :members:
+    :no-index:
diff --git a/lib/python/docs/index.rst b/lib/python/docs/index.rst
index e084d4c3b2..d1916a2e2a 100644
--- a/lib/python/docs/index.rst
+++ b/lib/python/docs/index.rst
@@ -1,18 +1,28 @@
-DBRepo Python Library
-=====================
+DBRepo Python Library documentation
+===================================
 
-.. image:: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/badges/release.svg
-    :alt: DBRepo latest release version
-    :target: https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services
+Use the DBRepo SDK to create, update, configure and manage DBRepo services such as the Data Service to get data as
+Pandas `DataFrame <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html>`_ for analysis. The DBRepo SDK
+provides an object-oriented API as well as low-level access to DBRepo services.
 
-.. image:: https://img.shields.io/pypi/dm/dbrepo
-    :alt: PyPI downloads per month
-    :target: https://pypi.org/project/dbrepo/__APPVERSION__/
+.. note::
+   The SDK has been implemented and documented for DBRepo version 1.4.2, earlier versions are not supported.
 
-.. warning::
-   This documentation is a work in progress.
+Quickstart
+----------
 
-REST Client
+Find numerous quickstart examples on
+the `DBRepo website <https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/__APPVERSION__/usage-overview/>`_.
+
+AMQP API Client
+-----------
+
+.. toctree::
+   :maxdepth: 2
+
+   guide/amqp-client
+
+REST API Client
 -----------
 
 .. toctree::
@@ -20,13 +30,13 @@ REST Client
 
    guide/rest-client
 
-AMQP Client
+Upload API Client
 -----------
 
 .. toctree::
    :maxdepth: 2
 
-   guide/amqp-client
+   guide/upload-client
 
 Indices and tables
 ==================
-- 
GitLab