Repository: arangoml/networkx-adapter (language: python)

Issue:
Consider a 'Verbose' option for adapter functionality
## Current state:
* Adapter functionality is performed silently, with minimal logs:
    * Log a "success" message before connecting to an ArangoDB endpoint
    * Log a "success" message at the end of an API call


## Proposed feature:
* Provide a `verbose` parameter option to all adapter API functionality, such that the package user is aware of all step-by-step operations performed:
    * ~~Log on attribute validation~~
    * Log on `__fetch_adb_docs` calls
    * ~~Log on Controller calls~~ (will leave this up to the user to configure if desired)
    * Log on NetworkX graph creation
    * Log on ArangoDB collection creation
    * Log on document insertions
    * etc.

## Sample

```py
adbnx_adapter.set_verbose(True)
nx_graph = adbnx_adapter.arangodb_graph_to_networkx("fraud-detection")
```

Not sure yet if this should be implemented, so will leave it with a `question` label for now
Expose ArangoClient & StandardDatabase from adapter to provide driver client accessibility
## Current Functionality

```python
from adbnx_adapter.adapter import ADBNX_Adapter # pip install adbnx-adapter

con = {
    "hostname": "localhost",
    "protocol": "http",
    "port": 8529,
    "username": "root",
    "password": "rootpassword",
    "dbName": "_system",
}

adbnx_adapter = ADBNX_Adapter(con) # no direct access to ArangoClient or StandardDatabase constructor 
```

## Proposed Functionality
```python
from arango import ArangoClient # pip install python-arango
from adbnx_adapter.adapter import ADBNX_Adapter # pip install adbnx-adapter

# full access to ArangoClient and StandardDatabase constructor, implying that the user can instantiate the client however they want (i.e custom HTTP client, serializer, deserializer, etc..) 
db = ArangoClient(hosts="http://localhost:8529").db("_system", username="root", password="rootpassword") 

adbnx_adapter = ADBNX_Adapter(db)
```


Would this not make more sense?

Consider a 'Verbose' option for adapter functionality
## Current state:
* Adapter functionality is performed silently, with minimal logs:
    * Log a "success" message before connecting to an ArangoDB endpoint
    * Log a "success" message at the end of an API call


## Proposed feature:
* Provide a `verbose` parameter option to all adapter API functionality, such that the package user is aware of all step-by-step operations performed:
    * ~~Log on attribute validation~~
    * Log on `__fetch_adb_docs` calls
    * ~~Log on Controller calls~~ (will leave this up to the user to configure if desired)
    * Log on NetworkX graph creation
    * Log on ArangoDB collection creation
    * Log on document insertions
    * etc.

## Sample

```py
adbnx_adapter.set_verbose(True)
nx_graph = adbnx_adapter.arangodb_graph_to_networkx("fraud-detection")
```

Not sure yet if this should be implemented, so will leave it with a `question` label for now


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 90c79493..d542ccc6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -103,9 +103,6 @@ jobs:
       - name: Install release packages
         run: pip install wheel gitchangelog pystache
 
-      - name: Install dependencies
-        run: pip install .[dev]
-
       - name: Set variables
         run: echo "VERSION=$(curl ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/latest | python -c "import sys; import json; print(json.load(sys.stdin)['tag_name'])")" >> $GITHUB_ENV
 
@@ -131,4 +128,4 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Alert developer of open PR
-        run: echo "Hi dev! Please go see $PR_URL"
+        run: echo "Changelog $PR_URL is ready to be merged by developer."
diff --git a/README.md b/README.md
index bc2a6ad7..91e06f04 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ For a more detailed walk-through, access the official notebook on Colab: <a href
 
 ```py
 # Import the ArangoDB-NetworkX Adapter
-from adbnx_adapter.adapter import ADBNX_Adapter
+from adbnx_adapter import ADBNX_Adapter
 
 # Import the Python-Arango driver
 from arango import ArangoClient
diff --git a/adbnx_adapter/__init__.py b/adbnx_adapter/__init__.py
index e69de29b..0d862d42 100644
--- a/adbnx_adapter/__init__.py
+++ b/adbnx_adapter/__init__.py
@@ -0,0 +1,2 @@
+from adbnx_adapter.adapter import ADBNX_Adapter  # noqa: F401
+from adbnx_adapter.controller import ADBNX_Controller  # noqa: F401
diff --git a/adbnx_adapter/adapter.py b/adbnx_adapter/adapter.py
index 91428183..132aafdf 100644
--- a/adbnx_adapter/adapter.py
+++ b/adbnx_adapter/adapter.py
@@ -1,8 +1,8 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
-
+import logging
 from collections import defaultdict
-from typing import Any, DefaultDict, Dict, List, Set, Tuple
+from typing import Any, DefaultDict, Dict, List, Set, Tuple, Union
 
 from arango.cursor import Cursor
 from arango.database import Database
@@ -15,6 +15,7 @@
 from .abc import Abstract_ADBNX_Adapter
 from .controller import ADBNX_Controller
 from .typings import ArangoMetagraph, Json, NxData, NxId
+from .utils import logger
 
 
 class ADBNX_Adapter(Abstract_ADBNX_Adapter):
@@ -26,14 +27,20 @@ class ADBNX_Adapter(Abstract_ADBNX_Adapter):
         and prepare nodes & edges before insertion, optionally re-defined by the user
         if needed (otherwise defaults to ADBNX_Controller).
     :type controller: ADBNX_Controller
-    :raise ValueError: If missing required keys in conn
+    :param logging_lvl: Defaults to logging.INFO. Other useful options are
+        logging.DEBUG (more verbose), and logging.WARNING (less verbose).
+    :type logging_lvl: str | int
+    :raise ValueError: If invalid parameters
     """
 
     def __init__(
         self,
         db: Database,
         controller: ADBNX_Controller = ADBNX_Controller(),
+        logging_lvl: Union[str, int] = logging.INFO,
     ):
+        self.set_logging(logging_lvl)
+
         if issubclass(type(db), Database) is False:
             msg = "**db** parameter must inherit from arango.database.Database"
             raise TypeError(msg)
@@ -45,10 +52,15 @@ def __init__(
         self.__db = db
         self.__cntrl: ADBNX_Controller = controller
 
+        logger.info(f"Instantiated ADBNX_Adapter with database '{db.name}'")
+
     @property
     def db(self) -> Database:
         return self.__db
 
+    def set_logging(self, level: Union[int, str]) -> None:
+        logger.setLevel(level)
+
     def arangodb_to_networkx(
         self,
         name: str,
@@ -91,6 +103,7 @@ def arangodb_to_networkx(
             },
         }
         """
+        logger.debug(f"Starting arangodb_to_networkx({name}, ...):")
         self.__validate_attributes("graph", set(metagraph), self.METAGRAPH_ATRIBS)
 
         # Maps ArangoDB vertex IDs to NetworkX node IDs
@@ -102,6 +115,7 @@ def arangodb_to_networkx(
 
         adb_v: Json
         for col, atribs in metagraph["vertexCollections"].items():
+            logger.debug(f"Preparing '{col}' vertices")
             for adb_v in self.__fetch_adb_docs(col, atribs, is_keep, query_options):
                 adb_id: str = adb_v["_id"]
                 nx_id = self.__cntrl._prepare_arangodb_vertex(adb_v, col)
@@ -111,6 +125,7 @@ def arangodb_to_networkx(
 
         adb_e: Json
         for col, atribs in metagraph["edgeCollections"].items():
+            logger.debug(f"Preparing '{col}' edges")
             for adb_e in self.__fetch_adb_docs(col, atribs, is_keep, query_options):
                 from_node_id: NxId = adb_map[adb_e["_from"]]["nx_id"]
                 to_node_id: NxId = adb_map[adb_e["_to"]]["nx_id"]
@@ -118,10 +133,11 @@ def arangodb_to_networkx(
 
                 nx_edges.append((from_node_id, to_node_id, adb_e))
 
+        logger.debug(f"Inserting {len(nx_nodes)} vertices and {len(nx_edges)} edges")
         nx_graph.add_nodes_from(nx_nodes)
         nx_graph.add_edges_from(nx_edges)
 
-        print(f"NetworkX: {name} created")
+        logger.info(f"Created NetworkX '{name}' Graph")
         return nx_graph
 
     def arangodb_collections_to_networkx(
@@ -222,6 +238,7 @@ def networkx_to_arangodb(
             }
         ]
         """
+        logger.debug(f"Starting networkx_to_arangodb('{name}', ...):")
         for e_d in edge_definitions:
             self.__validate_attributes(
                 "Edge Definitions", set(e_d), self.EDGE_DEFINITION_ATRIBS
@@ -236,6 +253,7 @@ def networkx_to_arangodb(
             adb_e_cols.add(e_col)
 
             if self.__db.has_collection(e_col) is False:
+                logger.debug(f"Creating {e_col} edge collection")
                 self.__db.create_collection(e_col, edge=True)
 
             v_col: str
@@ -244,11 +262,13 @@ def networkx_to_arangodb(
             for v_col in from_collections | to_collections:
                 adb_v_cols.add(v_col)
                 if self.__db.has_collection(v_col) is False:
+                    logger.debug(f"Creating {v_col} vertex collection")
                     self.__db.create_collection(v_col)
 
         is_homogeneous = len(adb_v_cols | adb_e_cols) == 2
         adb_v_col = adb_v_cols.pop() if is_homogeneous else None
         adb_e_col = adb_e_cols.pop() if is_homogeneous else None
+        logger.debug(f"Is graph '{name}' homogenous? {is_homogeneous}")
 
         self.__db.delete_graph(name, ignore_missing=True)
         adb_graph: ArangoDBGraph = self.__db.create_graph(name, edge_definitions)
@@ -256,6 +276,7 @@ def networkx_to_arangodb(
 
         nx_id: NxId
         nx_node: NxData
+        logger.debug(f"Preparing {len(nx_graph.nodes)} NetworkX nodes")
         for i, (nx_id, nx_node) in enumerate(nx_graph.nodes(data=True)):
             col = adb_v_col or self.__cntrl._identify_networkx_node(nx_id, nx_node)
             key = (
@@ -272,6 +293,7 @@ def networkx_to_arangodb(
         from_node_id: NxId
         to_node_id: NxId
         nx_edge: NxData
+        logger.debug(f"Preparing {len(nx_graph.edges)} NetworkX edges")
         for i, (from_node_id, to_node_id, nx_edge) in enumerate(
             nx_graph.edges(data=True)
         ):
@@ -302,9 +324,10 @@ def networkx_to_arangodb(
             self.__insert_adb_docs(col, adb_documents[col], nx_edge, batch_size)
 
         for col, doc_list in adb_documents.items():  # insert remaining documents
+            logger.debug(f"Inserting last {len(doc_list)} documents into '{col}'")
             self.__db.collection(col).import_bulk(doc_list, on_duplicate="replace")
 
-        print(f"ArangoDB: {name} created")
+        logger.info(f"Created ArangoDB '{name}' Graph")
         return adb_graph
 
     def __validate_attributes(
@@ -379,5 +402,6 @@ def __insert_adb_docs(
         col_docs.append(doc)
 
         if len(col_docs) >= batch_size:
+            logger.debug(f"Inserting next {batch_size} batch documents into '{col}'")
             self.__db.collection(col).import_bulk(col_docs, on_duplicate="replace")
             col_docs.clear()
diff --git a/adbnx_adapter/utils.py b/adbnx_adapter/utils.py
new file mode 100644
index 00000000..3f3f894f
--- /dev/null
+++ b/adbnx_adapter/utils.py
@@ -0,0 +1,11 @@
+import logging
+import os
+
+logger = logging.getLogger(__package__)
+handler = logging.StreamHandler()
+formatter = logging.Formatter(
+    f"[%(asctime)s] [{os.getpid()}] [%(levelname)s] - %(name)s: %(message)s",
+    "%Y/%m/%d %H:%M:%S %z",
+)
+handler.setFormatter(formatter)
+logger.addHandler(handler)
diff --git a/setup.cfg b/setup.cfg
index 8494e52d..04d8dcef 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -28,5 +28,6 @@ extend-ignore = E203, E741, W503
 exclude =.git .idea .*_cache dist venv
 
 [mypy]
-ignore_missing_imports = True
 strict = True
+ignore_missing_imports = True
+implicit_reexport = True


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 132712dc..1490a47b 100755
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,5 @@
 import io
+import logging
 import os
 import subprocess
 import urllib.request as urllib
@@ -11,8 +12,7 @@
 from networkx import grid_2d_graph, parse_gml
 from networkx.classes import Graph as NetworkXGraph
 
-from adbnx_adapter.adapter import ADBNX_Adapter
-from adbnx_adapter.controller import ADBNX_Controller
+from adbnx_adapter import ADBNX_Adapter, ADBNX_Controller
 from adbnx_adapter.typings import Json, NxData, NxId
 
 PROJECT_DIR = Path(__file__).parent.parent
@@ -52,10 +52,16 @@ def pytest_configure(config: Any) -> None:
     )
 
     global adbnx_adapter, imdb_adbnx_adapter, grid_adbnx_adapter, football_adbnx_adapter
-    adbnx_adapter = ADBNX_Adapter(db)
-    imdb_adbnx_adapter = ADBNX_Adapter(db, IMDB_ADBNX_Controller())
-    grid_adbnx_adapter = ADBNX_Adapter(db, Grid_ADBNX_Controller())
-    football_adbnx_adapter = ADBNX_Adapter(db, Football_ADBNX_Controller())
+    adbnx_adapter = ADBNX_Adapter(db, logging_lvl=logging.DEBUG)
+    imdb_adbnx_adapter = ADBNX_Adapter(
+        db, IMDB_ADBNX_Controller(), logging_lvl=logging.DEBUG
+    )
+    grid_adbnx_adapter = ADBNX_Adapter(
+        db, Grid_ADBNX_Controller(), logging_lvl=logging.DEBUG
+    )
+    football_adbnx_adapter = ADBNX_Adapter(
+        db, Football_ADBNX_Controller(), logging_lvl=logging.DEBUG
+    )
 
     arango_restore(con, "examples/data/fraud_dump")
     arango_restore(con, "examples/data/imdb_dump")
@@ -96,15 +102,6 @@ def arango_restore(con: Json, path_to_data: str) -> None:
     )
 
 
-def print_connection_details(con: Json) -> None:
-    print("----------------------------------------")
-    print("https://{}:{}".format(con["hostname"], con["port"]))
-    print("Username: " + con["username"])
-    print("Password: " + con["password"])
-    print("Database: " + con["dbName"])
-    print("----------------------------------------")
-
-
 def get_grid_graph() -> NetworkXGraph:
     return grid_2d_graph(5, 5)
 
diff --git a/tests/test_adapter.py b/tests/test_adapter.py
index 8cb42742..1d717c53 100644
--- a/tests/test_adapter.py
+++ b/tests/test_adapter.py
@@ -1,11 +1,11 @@
+import logging
 from typing import Any, Dict, List, Set
 
 import pytest
 from arango.graph import Graph as ArangoGraph
 from networkx.classes.graph import Graph as NxGraph
 
-from adbnx_adapter.adapter import ADBNX_Adapter
-from adbnx_adapter.controller import ADBNX_Controller
+from adbnx_adapter import ADBNX_Adapter, ADBNX_Controller
 from adbnx_adapter.typings import ArangoMetagraph, Json, NxData, NxId
 
 from .conftest import (
@@ -32,7 +32,7 @@ class Bad_ADBNX_Controller:
         pass
 
     with pytest.raises(TypeError):
-        ADBNX_Adapter(bad_db, Bad_ADBNX_Controller())  # type: ignore
+        ADBNX_Adapter(bad_db)
 
     with pytest.raises(TypeError):
         ADBNX_Adapter(db, Bad_ADBNX_Controller())  # type: ignore
@@ -276,7 +276,9 @@ def _identify_networkx_edge(
             adb_vertex_id: str = str(nx_edge["_id"])
             return adb_vertex_id.split("/")[0] + "_new"
 
-    fraud_adbnx_adapter = ADBNX_Adapter(db, Fraud_ADBNX_Controller())
+    fraud_adbnx_adapter = ADBNX_Adapter(
+        db, Fraud_ADBNX_Controller(), logging_lvl=logging.DEBUG
+    )
 
     new_fraud_adb_g = fraud_adbnx_adapter.networkx_to_arangodb(
         name + "_new",