Skip to content

Refactor with updates#1

Merged
jabez007 merged 41 commits intomasterfrom
refactor-with-updates
Apr 24, 2025
Merged

Refactor with updates#1
jabez007 merged 41 commits intomasterfrom
refactor-with-updates

Conversation

@jabez007
Copy link
Owner

@jabez007 jabez007 commented Apr 7, 2025

Summary by CodeRabbit

  • New Features

    • Released Extended Odoo Client Library (v2.0.2) with improved connection and authentication management.
    • Added support for secure and non-secure XMLRPC and JSONRPC connectors with synchronous and asynchronous operations.
    • Introduced dynamic service and model interaction with enhanced proxy methods and context handling.
    • Provided Docker Compose setup and configuration files for easy local Odoo and PostgreSQL deployment.
    • Added GitHub Actions workflows for automated unit testing across multiple Python versions and publishing to TestPyPI and PyPI.
  • Tests

    • Expanded unit test coverage with new async tests and connector-specific tests.
    • Updated tests to reflect new package namespace and removed deprecated tests.
  • Chores

    • Modernized project configuration with updated pyproject.toml, enhanced setup.py, and new requirements.txt.
    • Added comprehensive .gitignore covering Python, packaging, testing, and development artifacts.
    • Updated README with new package name and local testing instructions.

@jabez007 jabez007 self-assigned this Apr 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Apr 7, 2025

Walkthrough

This pull request adds a new GitHub Actions workflow and a comprehensive .gitignore file, updates package configuration files, and overhauls the Odoo client library. The old implementation is removed and replaced with new architecture files for connection management, RPC handling, model interaction, and service abstraction—including both synchronous and asynchronous routines. In addition, Docker configurations for Odoo and PostgreSQL services are introduced along with initialization scripts, and the test suite is expanded to cover the new functionality.

Changes

File(s) Change Summary
.github/workflows/test.yml, .github/workflows/test-publish.yml, .github/workflows/publish.yml, .gitignore Added CI workflows for automated unit tests, publishing to TestPyPI and PyPI, plus a new .gitignore file with patterns to exclude unnecessary files.
pyproject.toml, requirements.txt, setup.py Added and updated project metadata, dependencies, build-system settings, and package configuration to modern standards with namespace packages and extended Python version support.
odoolib/main.py (removed) Removed the old monolithic Odoo client library implementation.
src/mccann_hub/odoolib/main.py, src/mccann_hub/odoolib/connection/*, src/mccann_hub/odoolib/connector/*, src/mccann_hub/odoolib/model.py, src/mccann_hub/odoolib/service.py Introduced new modular classes for Connection, Servicer, AuthenticationError, Connector, Sender, Service, and protocol-specific connectors (JSONRPC and XMLRPC), supporting synchronous and asynchronous RPC calls.
test_containers/compose.yaml, test_containers/config/odoo.conf, test_containers/postgres/Dockerfile, test_containers/postgres/docker-ensure-initdb.sh, test_containers/postgres/docker-entrypoint.sh Added Docker Compose setup and configuration files for Odoo and PostgreSQL containers with health checks and initialization scripts.
tests/*.py Added and refactored extensive unit tests covering synchronous and asynchronous operations, connectors, connection management, model interaction, and service abstractions.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant M as Odoo Client (main)
    participant G as get_connector
    participant CN as Connector
    participant CO as Connection
    participant S as Servicer

    C->>M: get_connection(hostname, protocol, port, …)
    M->>G: get_connector(hostname, protocol, port)
    G-->>M: Return appropriate Connector (XMLRPC/JSONRPC)
    M->>CO: Instantiate Connection(connector, database, login, …)
    CO->>S: Delegate authentication & context retrieval
    S-->>CO: Return user context or model service
    CO-->>M: Return established Connection
    M-->>C: Connection ready for RPC calls
Loading
sequenceDiagram
    participant C as Client
    participant Srv as Service
    participant Sd as Sender
    participant RS as Remote Service

    C->>Srv: Invoke method (e.g., read or search_read)
    Srv->>Srv: __getattr__ intercepts call and creates proxy
    Srv->>Sd: Calls send (or async_send) with service/method & args
    Sd->>RS: Forward RPC request
    RS-->>Sd: Return response/result
    Sd-->>Srv: Send back result
    Srv-->>C: Return method call result
Loading

Poem

Oh, how I hop with glee in code so neat,
New workflows and connectors make my heart beat.
From Docker dreams to tests that shine,
Async and sync, all working in line.
I nibble bugs away with a happy little cheer,
A coding bunny celebrating changes far and near! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (42)
src/mccann_hub/odoolib/connection/authentication_error.py (1)

8-8: Update the copyright year to the current year

The copyright year is set to 2025, which is in the future. Update it to the current year (2024).

- Copyright (C) 2025 Jimmy McCann
+ Copyright (C) 2024 Jimmy McCann
src/mccann_hub/odoolib/connector/json_rpcs.py (4)

8-8: Verify copyright year

The copyright year is set to 2025, which is in the future. Consider updating to the current year.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann

36-41: Update class docstring to mention secure protocol

The current docstring states that the class uses "the JsonRPC protocol", but it doesn't mention that it specifically uses the secure HTTPS variant. Update the docstring to be more specific about the secure nature of this connector.

class JsonRpcsConnector(JsonRpcConnector):
    """
-   A type of connector that uses the JsonRPC protocol.
+   A type of connector that uses the secure JsonRPC protocol over HTTPS.
    """

    PROTOCOL = "jsonrpcs"

43-50: Add version parameter to docstring

The version parameter is missing from the docstring. Include it to fully document all parameters.

    def __init__(self, hostname: str, port=8069, version="2.0"):
        """
        Initialize by specifying the hostname and the port.
        :param hostname: The hostname of the computer holding the instance of Odoo.
        :param port: The port used by the Odoo instance for JsonRPC (default to 8069).
+       :param version: The JsonRPC protocol version (default to "2.0").
        """
        super(JsonRpcsConnector, self).__init__(hostname, port, version)
        self.url: str = "https://%s:%d/jsonrpc" % (hostname, port)

49-50: Avoid overriding parent class attributes

The url attribute is set in the parent class constructor but then immediately overridden here. This pattern can be inefficient and error-prone if the parent class behavior changes. Consider alternative approaches to avoid duplication.

    def __init__(self, hostname: str, port=8069, version="2.0"):
        """
        Initialize by specifying the hostname and the port.
        :param hostname: The hostname of the computer holding the instance of Odoo.
        :param port: The port used by the Odoo instance for JsonRPC (default to 8069).
        """
-       super(JsonRpcsConnector, self).__init__(hostname, port, version)
-       self.url: str = "https://%s:%d/jsonrpc" % (hostname, port)
+       # Initialize parent without setting url
+       super(JsonRpcConnector, self).__init__()
+       self.version = version
+       self.url: str = "https://%s:%d/jsonrpc" % (hostname, port)

Alternatively, you could modify the parent class to accept a protocol parameter that determines whether to use HTTP or HTTPS.

src/mccann_hub/odoolib/dates.py (1)

36-39: Consistent string formatting approach

The formatting changes to DEFAULT_SERVER_DATETIME_FORMAT improve readability and consistency. Consider using an f-string for modern Python formatting if you're using Python 3.6+.

-DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
-    DEFAULT_SERVER_DATE_FORMAT,
-    DEFAULT_SERVER_TIME_FORMAT,
-)
+DEFAULT_SERVER_DATETIME_FORMAT = f"{DEFAULT_SERVER_DATE_FORMAT} {DEFAULT_SERVER_TIME_FORMAT}"
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)

53-58: Consider reducing URL construction duplication

The URL construction code is duplicated from the parent class with only the protocol change from "http" to "https". Consider refactoring to avoid duplication by extending the parent's implementation.

 def __init__(
     self,
     hostname: str,
     port=8069,
     version: Optional[str] = "2",
     transport: Optional[SafeTransport] = None,
 ):
     super(XmlRpcsConnector, self).__init__(hostname, port, version, transport)
-    self.url = (
-        "https://%s:%d/xmlrpc" % (hostname, port)
-        if version is None
-        else "https://%s:%d/xmlrpc/%s" % (hostname, port, version)
-    )
+    # Replace 'http' with 'https' in the URL
+    self.url = self.url.replace("http://", "https://")
tests/test_xml_rpc.py (1)

42-50: Consider adding tests for different connector configurations

The current test only verifies one configuration (default port and with version). Consider adding tests for different parameter combinations like custom port or no version.

def test_initialization_with_custom_port(self):
    connector = XmlRpcConnector(self.hostname, port=9000)
    self.assertEqual(
        connector.url,
        f"http://{self.hostname}:9000/xmlrpc/{self.version}",
    )

def test_initialization_without_version(self):
    connector = XmlRpcConnector(self.hostname, version=None)
    self.assertEqual(
        connector.url,
        f"http://{self.hostname}:{self.port}/xmlrpc",
    )
test_containers/compose.yaml (1)

24-24: Remove trailing whitespace

There's a trailing space at the end of line 24.

-      - POSTGRES_PASSWORD=odoo 
+      - POSTGRES_PASSWORD=odoo
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 24-24: trailing spaces

(trailing-spaces)

src/mccann_hub/odoolib/connector/_connector.py (2)

47-52: Consider simplifying the logger name construction

The logger name construction is somewhat complex with string splitting and joining. Consider simplifying this for better readability.

-    def __init__(self):
-        self._logger = logging.getLogger(
-            f"{str.join('.', __name__.split('.')[:-1])}"
-            + (f".{self.PROTOCOL}" if self.PROTOCOL is not None else "")
-        )
-        self.url: Optional[str] = None
+    def __init__(self):
+        # Get the package name (without the module part)
+        package_name = ".".join(__name__.split(".")[:-1])
+        logger_name = package_name
+        if self.PROTOCOL is not None:
+            logger_name = f"{logger_name}.{self.PROTOCOL}"
+        self._logger = logging.getLogger(logger_name)
+        self.url: Optional[str] = None

8-8: Future copyright date

The copyright notice includes the year 2025, which is a future date.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
tests/test_json_rpc.py (1)

4-4: Future copyright date

The copyright notice includes the year 2025, which is a future date.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
.github/workflows/test.yml (2)

26-28: Consider adding container cleanup step

The workflow starts Docker containers but doesn't explicitly shut them down after tests, which could leave resources running unnecessarily.

      - name: Start up containers
        run: |
          docker compose -f ./test_containers/compose.yaml up --detach --wait --wait-timeout 600
+
+      # Other steps...
+
+      - name: Cleanup containers
+        if: always()
+        run: |
+          docker compose -f ./test_containers/compose.yaml down --volumes --remove-orphans

35-37: Consider adding dependency caching

Adding caching for Python dependencies can significantly speed up workflow execution by reusing previously installed packages.

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python_version }}

+      - name: Cache pip dependencies
+        uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ matrix.python_version }}-${{ hashFiles('requirements.txt') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-${{ matrix.python_version }}-
+            ${{ runner.os }}-pip-

      - name: Install requirements
        run: |
          pip install -r requirements.txt
tests/test_json_rpcs.py (2)

52-88: Consider refactoring duplicate test code

The test methods test_send_success and test_send_error are nearly identical to those in test_json_rpc.py. Consider creating a base test class with common functionality to reduce duplication.

# Example refactoring approach:
import unittest
from unittest.mock import MagicMock, patch
import json

class BaseJsonRpcConnectorTest(unittest.TestCase):
    connector_class = None  # To be overridden by subclasses
    protocol = None         # To be overridden by subclasses
    
    def setUp(self):
        self.hostname = "localhost"
        self.port = 8069
        self.version = "2.0"
        self.connector = self.connector_class(self.hostname, self.port, self.version)
    
    def test_send_success(self):
        with patch(f"mccann_hub.odoolib.connector.json_rpc.requests.post") as mock_post:
            mock_response = MagicMock()
            mock_response.json.return_value = {"result": "mock_response"}
            mock_post.return_value = mock_response
            
            response = self.connector.send("common", "some_method", "arg1", "arg2")
            
            # Verify request data
            _, called_kwargs = mock_post.call_args
            sent_data = json.loads(called_kwargs["data"])
            
            # Verify all expected fields except 'id'
            self.assertEqual(sent_data["jsonrpc"], self.version)
            self.assertEqual(sent_data["method"], "call")
            self.assertEqual(
                sent_data["params"],
                {"service": "common", "method": "some_method", "args": ["arg1", "arg2"]},
            )
            
            # Ensure 'id' exists and is an integer
            self.assertIn("id", sent_data)
            self.assertIsInstance(sent_data["id"], int)
            
            self.assertEqual(response, "mock_response")
    
    def test_send_error(self):
        with patch(f"mccann_hub.odoolib.connector.json_rpc.requests.post") as mock_post:
            mock_response = MagicMock()
            mock_response.json.return_value = {"error": "mock_error"}
            mock_post.return_value = mock_response
            
            with self.assertRaises(JsonRpcException) as context:
                self.connector.send("common", "some_method", "arg1", "arg2")
            
            self.assertEqual(str(context.exception), "'mock_error'")

# Then in test_json_rpc.py:
class TestJsonRpcConnector(BaseJsonRpcConnectorTest):
    connector_class = JsonRpcConnector
    protocol = "jsonrpc"
    
    def test_initialization(self):
        self.assertEqual(
            self.connector.url, f"http://{self.hostname}:{self.port}/jsonrpc"
        )
        self.assertEqual(self.connector.version, self.version)
        self.assertEqual(
            self.connector._logger.name, f"mccann_hub.odoolib.connector.{self.protocol}"
        )

# And in test_json_rpcs.py:
class TestJsonRpcsConnector(BaseJsonRpcConnectorTest):
    connector_class = JsonRpcsConnector
    protocol = "jsonrpcs"
    
    def test_initialization(self):
        self.assertEqual(
            self.connector.url, f"https://{self.hostname}:{self.port}/jsonrpc"
        )
        self.assertEqual(self.connector.version, self.version)
        self.assertEqual(
            self.connector._logger.name, f"mccann_hub.odoolib.connector.{self.protocol}"
        )

4-4: Future copyright date

The copyright notice includes the year 2025, which is a future date.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
src/mccann_hub/odoolib/main.py (4)

8-8: Update the copyright year to the current year.

The copyright date is set to 2025, which is in the future. Consider updating it to the current year.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann

33-38: Consider updating the docstring with your own repository information.

The module docstring still contains references to the original Odoo client library's URLs. It would be more accurate to update this with your repository information if this is a fork or customized version.


52-61: Improve conditional flow consistency.

The conditional structure switches from if/elif to a fresh if/elif pattern. This works functionally but is stylistically inconsistent.

 if protocol == "xmlrpc":
     return XmlRpcConnector(hostname, port)
 elif protocol == "xmlrpcs":
     return XmlRpcsConnector(hostname, port)
-if protocol == "jsonrpc":
+elif protocol == "jsonrpc":
     return JsonRpcConnector(hostname, port)
 elif protocol == "jsonrpcs":
     return JsonRpcsConnector(hostname, port)
 else:
     raise ValueError("You must choose xmlrpc, xmlrpcs, jsonrpc or jsonrpcs")

73-85: Fix incorrect parameter documentation.

The docstring mentions a connector parameter that doesn't exist in the function signature. The connector is created internally using get_connector.

 """
 A shortcut method to easily create a connection to a remote Odoo server.

 :param hostname: The hostname to the remote server.
 :param protocol: The name of the protocol, must be "xmlrpc", "xmlrpcs", "jsonrpc" or "jsonrpcs".
 :param port: The number of the port. Defaults to 8069.
-:param connector: A valid Connector instance to send messages to the remote server.
 :param database: The name of the database to work on.
 :param login: The login of the user.
 :param password: The password of the user.
 :param user_id: The user id is a number identifying the user. This is only useful if you
 already know it, in most cases you don't need to specify it.
 """
src/mccann_hub/odoolib/service.py (2)

8-8: Update the copyright year to the current year.

The copyright date is set to 2025, which is in the future. Consider updating it to the current year.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann

52-78: Consider adding error handling for RPC calls.

The proxy functions do not include any error handling for failures that might occur during RPC calls. Consider adding try/except blocks to handle and properly log potential errors.

def proxy(*args):
    """
    :param args: A list of values for the method
    """
    self._logger.debug("args (sync): %r", args)
-   result = self.connector.send(self.service_name, method, *args)
-   self._logger.debug("result (sync): %r", result)
-   return result
+   try:
+       result = self.connector.send(self.service_name, method, *args)
+       self._logger.debug("result (sync): %r", result)
+       return result
+   except Exception as e:
+       self._logger.error("Error calling %s.%s: %s", self.service_name, method, e)
+       raise

async def async_proxy(*args):
    """
    :param args: A list of values for the method
    """
    self._logger.debug("args (async): %r", args)
-   result = await self.connector.async_send(self.service_name, method, *args)
-   self._logger.debug("result (async): %r", result)
-   return result
+   try:
+       result = await self.connector.async_send(self.service_name, method, *args)
+       self._logger.debug("result (async): %r", result)
+       return result
+   except Exception as e:
+       self._logger.error("Error calling %s.%s asynchronously: %s", self.service_name, method, e)
+       raise
tests/test_async.py (2)

4-4: Update the copyright year to the current year.

The copyright date is set to 2025, which is in the future. Consider updating it to the current year.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann

38-45: Extract hardcoded connection parameters to constants or configuration.

The connection parameters are hardcoded, which makes the tests less flexible and harder to run in different environments. Consider extracting these to constants at the top of the file, or better yet, use environment variables.

+# At the top of the file
+# Default connection parameters - can be overridden by environment variables
+HOST = os.environ.get("ODOO_TEST_HOST", "localhost")
+DATABASE = os.environ.get("ODOO_TEST_DB", "test")
+LOGIN = os.environ.get("ODOO_TEST_LOGIN", "admin")
+PASSWORD = os.environ.get("ODOO_TEST_PASSWORD", "a")

def _conn(self, protocol):
    return odoolib.get_connection(
-       hostname="localhost",
+       hostname=HOST,
        protocol=protocol,
-       database="test",
-       login="admin",
-       password="a",
+       database=DATABASE,
+       login=LOGIN,
+       password=PASSWORD,
    )
tests/test_connection.py (1)

1-27: Copyright year in the future

The copyright notice shows 2025 which is a future date (current year is 2024).

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
src/mccann_hub/odoolib/connection/_connection.py (1)

1-31: Copyright year in the future

The copyright notice includes 2025 which is a future date (current year is 2024).

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
tests/test_xml_rpcs.py (1)

1-27: Copyright year in the future

The copyright notice shows 2025 which is a future date (current year is 2024).

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
tests/test_model.py (1)

1-27: Copyright year in the future

The copyright notice shows 2025 which is a future date (current year is 2024).

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
src/mccann_hub/odoolib/connector/xml_rpc.py (1)

39-65: Class initialization looks good, consider adding type annotations

The XmlRpcConnector class is well-structured with clear documentation, but missing some type annotations:

    def __init__(
        self,
        hostname: str,
-       port=8069,
+       port: int = 8069,
        version: Optional[str] = "2",
        transport: Optional[Transport] = None,
    ):
tests/test.py (1)

61-84: Test cases maintained but could be expanded

The test methods are maintained with minor formatting improvements, but they are minimal and might not provide enough coverage for the library's functionality.

Consider adding more comprehensive tests for:

  1. Error handling scenarios
  2. Different authentication methods
  3. Tests for the new connector implementations
  4. Edge cases for different API calls
src/mccann_hub/odoolib/connector/json_rpc.py (2)

56-65: Add type annotations to constructor parameters

Similar to the XmlRpcConnector, some type annotations are missing:

-    def __init__(self, hostname: str, port=8069, version="2.0"):
+    def __init__(self, hostname: str, port: int = 8069, version: str = "2.0"):

66-69: Add documentation to the send method

The send method lacks documentation describing its purpose and parameters:

-    def send(self, service_name: str, method: str, *args):
+    def send(self, service_name: str, method: str, *args) -> any:
+        """
+        Send a request to the specified service and method with the given arguments.
+        
+        :param service_name: The name of the service to call
+        :param method: The method name to call on the service
+        :param args: Additional arguments to pass to the method
+        :return: The result of the method call
+        :raises: JsonRpcException if the remote call returns an error
+        """
tests/test_service.py (1)

54-63: Consider renaming the test to avoid referencing 'caching'
Currently, this test validates multiple calls but does not actually test caching. Renaming it for clarity or adding a caching mechanism would better reflect the test objective.

-def test_sync_method_caching_and_multiple_calls(self):
+def test_sync_method_multiple_calls(self):
test_containers/postgres/docker-ensure-initdb.sh (1)

30-30: Use $BASH_SOURCE[0] for better clarity and to address ShellCheck SC2128.
Expanding $BASH_SOURCE without an index can cause unintended behavior in certain shells or nested calls.

-exec gosu postgres "$BASH_SOURCE" "$@"
+exec gosu postgres "${BASH_SOURCE[0]}" "$@"
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 30-30: Expanding an array without an index only gives the first element.

(SC2128)

src/mccann_hub/odoolib/model.py (1)

98-115: Refactor nested conditional checks.
You can combine if method == "read" and if isinstance(result, list) ... into a single check for slightly cleaner code.

-if method == "read":
-    if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
+if method == "read" and isinstance(result, list) and len(result) > 0 and "id" in result[0]:
     index = {}
     ...
🧰 Tools
🪛 Ruff (0.8.2)

102-103: Use a single if statement instead of nested if statements

(SIM102)

src/mccann_hub/odoolib/connection/_servicer.py (3)

8-8: Check the copyright year.

The copyright year is set to 2025, which is in the future. Consider updating it to reflect the current year 2024.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann

67-73: Remove incorrect parameter in docstring.

The connector parameter is mentioned in the docstring but it's not part of the method signature for set_login_info.

        """
        Set login information after the initialisation of this object.

-        :param connector: A valid Connector instance to send messages to the remote server.
        :param database: The name of the database to work on.
        :param login: The login of the user.
        :param password: The password of the user.
        :param user_id: The user id is a number identifying the user. This is only useful if you
        already know it, in most cases you don't need to specify it.
        """

78-91: Update docstring to reference Servicer.

The docstring references "Connection" but should reference "Servicer" class since that's the actual class name.

        """
-        Checks if this Connection was already validated previously.
+        Checks if this Servicer was already validated previously.

        :param force: Force to re-check even if this Connection was already validated previously.
        Default to True.
        """
setup.py (1)

8-8: Check the copyright year.

The copyright year is set to 2025, which is in the future. Consider updating it to reflect the current year 2024.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
test_containers/postgres/docker-entrypoint.sh (3)

163-163: Unused variable warning.

The psql variable appears to be unused. If it's intended for external use, consider exporting it.

-	psql=( docker_process_sql )
+	local psql=( docker_process_sql ) # Define as local if only used in this function

or if used externally:

-	psql=( docker_process_sql )
+	export psql=( docker_process_sql )
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 163-163: psql appears unused. Verify use (or export if used externally).

(SC2034)


201-201: Fix syntax for empty variable assignment.

Remove spaces after = when assigning empty values. According to shellcheck, this can cause issues.

-	PGHOST= PGHOSTADDR= "${query_runner[@]}" "$@"
+	PGHOST="" PGHOSTADDR="" "${query_runner[@]}" "$@"

Similar fixes should be applied to lines 209, 214, and 274-275.

Also applies to: 209-209, 214-214, 274-275

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


316-316: Fix array expansion.

Expanding an array without an index only gives the first element. This might not be the intended behavior.

-			exec gosu postgres "$BASH_SOURCE" "$@"
+			exec gosu postgres "${BASH_SOURCE[0]}" "$@"
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 316-316: Expanding an array without an index only gives the first element.

(SC2128)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25b15ea and 17c96a2.

📒 Files selected for processing (35)
  • .github/workflows/test.yml (1 hunks)
  • .gitignore (1 hunks)
  • odoolib/main.py (0 hunks)
  • pyproject.toml (1 hunks)
  • requirements.txt (1 hunks)
  • setup.py (2 hunks)
  • src/mccann_hub/odoolib/connection/__init__.py (1 hunks)
  • src/mccann_hub/odoolib/connection/_connection.py (1 hunks)
  • src/mccann_hub/odoolib/connection/_servicer.py (1 hunks)
  • src/mccann_hub/odoolib/connection/authentication_error.py (1 hunks)
  • src/mccann_hub/odoolib/connector/__init__.py (1 hunks)
  • src/mccann_hub/odoolib/connector/_connector.py (1 hunks)
  • src/mccann_hub/odoolib/connector/_sender.py (1 hunks)
  • src/mccann_hub/odoolib/connector/json_rpc.py (1 hunks)
  • src/mccann_hub/odoolib/connector/json_rpcs.py (1 hunks)
  • src/mccann_hub/odoolib/connector/xml_rpc.py (1 hunks)
  • src/mccann_hub/odoolib/connector/xml_rpcs.py (1 hunks)
  • src/mccann_hub/odoolib/dates.py (5 hunks)
  • src/mccann_hub/odoolib/main.py (1 hunks)
  • src/mccann_hub/odoolib/model.py (1 hunks)
  • src/mccann_hub/odoolib/service.py (1 hunks)
  • test_containers/compose.yaml (1 hunks)
  • test_containers/config/odoo.conf (1 hunks)
  • test_containers/postgres/Dockerfile (1 hunks)
  • test_containers/postgres/docker-ensure-initdb.sh (1 hunks)
  • test_containers/postgres/docker-entrypoint.sh (1 hunks)
  • tests/test.py (2 hunks)
  • tests/test_async.py (1 hunks)
  • tests/test_connection.py (1 hunks)
  • tests/test_json_rpc.py (1 hunks)
  • tests/test_json_rpcs.py (1 hunks)
  • tests/test_model.py (1 hunks)
  • tests/test_service.py (1 hunks)
  • tests/test_xml_rpc.py (1 hunks)
  • tests/test_xml_rpcs.py (1 hunks)
💤 Files with no reviewable changes (1)
  • odoolib/main.py
🧰 Additional context used
🧬 Code Definitions (19)
src/mccann_hub/odoolib/connector/__init__.py (4)
src/mccann_hub/odoolib/connector/json_rpc.py (2)
  • JsonRpcConnector (49-88)
  • JsonRpcException (41-46)
src/mccann_hub/odoolib/connector/json_rpcs.py (1)
  • JsonRpcsConnector (36-50)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • XmlRpcConnector (39-69)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
  • XmlRpcsConnector (39-58)
src/mccann_hub/odoolib/connection/__init__.py (2)
src/mccann_hub/odoolib/connection/_connection.py (1)
  • Connection (40-90)
src/mccann_hub/odoolib/connection/authentication_error.py (1)
  • AuthenticationError (34-39)
tests/test_xml_rpc.py (1)
src/mccann_hub/odoolib/connector/xml_rpc.py (2)
  • XmlRpcConnector (39-69)
  • send (66-69)
src/mccann_hub/odoolib/connector/json_rpcs.py (1)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • JsonRpcConnector (49-88)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • XmlRpcConnector (39-69)
tests/test_json_rpc.py (1)
src/mccann_hub/odoolib/connector/json_rpc.py (3)
  • JsonRpcConnector (49-88)
  • JsonRpcException (41-46)
  • send (66-69)
src/mccann_hub/odoolib/connector/_connector.py (3)
src/mccann_hub/odoolib/service.py (1)
  • Service (38-78)
src/mccann_hub/odoolib/connector/_sender.py (1)
  • Sender (33-51)
src/mccann_hub/odoolib/connection/_servicer.py (1)
  • get_service (131-139)
tests/test_async.py (3)
tests/test.py (2)
  • setUp (45-46)
  • _conn (48-55)
src/mccann_hub/odoolib/main.py (1)
  • get_connection (64-88)
src/mccann_hub/odoolib/connection/_connection.py (2)
  • get_model (84-90)
  • async_get_user_context (76-82)
src/mccann_hub/odoolib/main.py (5)
src/mccann_hub/odoolib/connection/_connection.py (1)
  • Connection (40-90)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • JsonRpcConnector (49-88)
src/mccann_hub/odoolib/connector/json_rpcs.py (1)
  • JsonRpcsConnector (36-50)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • XmlRpcConnector (39-69)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
  • XmlRpcsConnector (39-58)
src/mccann_hub/odoolib/connection/_connection.py (3)
src/mccann_hub/odoolib/connector/_connector.py (1)
  • Connector (40-60)
src/mccann_hub/odoolib/model.py (1)
  • Model (38-160)
src/mccann_hub/odoolib/connection/_servicer.py (1)
  • Servicer (41-139)
tests/test_json_rpcs.py (2)
src/mccann_hub/odoolib/connector/json_rpc.py (2)
  • JsonRpcException (41-46)
  • send (66-69)
src/mccann_hub/odoolib/connector/json_rpcs.py (1)
  • JsonRpcsConnector (36-50)
src/mccann_hub/odoolib/service.py (3)
src/mccann_hub/odoolib/connector/_sender.py (3)
  • Sender (33-51)
  • send (38-42)
  • async_send (44-51)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • send (66-69)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • send (66-69)
tests/test_service.py (1)
src/mccann_hub/odoolib/service.py (2)
  • Service (38-78)
  • proxy (58-65)
tests/test_xml_rpcs.py (2)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
  • XmlRpcsConnector (39-58)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • send (66-69)
tests/test_model.py (1)
src/mccann_hub/odoolib/model.py (2)
  • Model (38-160)
  • search_read (116-136)
tests/test.py (2)
src/mccann_hub/odoolib/main.py (1)
  • get_connection (64-88)
src/mccann_hub/odoolib/connection/_connection.py (2)
  • get_model (84-90)
  • get_user_context (68-74)
src/mccann_hub/odoolib/connection/_servicer.py (3)
src/mccann_hub/odoolib/connector/_connector.py (2)
  • Connector (40-60)
  • get_service (54-60)
src/mccann_hub/odoolib/service.py (1)
  • Service (38-78)
src/mccann_hub/odoolib/connection/authentication_error.py (1)
  • AuthenticationError (34-39)
tests/test_connection.py (2)
src/mccann_hub/odoolib/connection/_connection.py (3)
  • Connection (40-90)
  • get_model (84-90)
  • get_user_context (68-74)
src/mccann_hub/odoolib/model.py (1)
  • Model (38-160)
src/mccann_hub/odoolib/model.py (2)
src/mccann_hub/odoolib/connection/_servicer.py (4)
  • Servicer (41-139)
  • check_login (93-110)
  • get_service (131-139)
  • async_check_login (112-129)
src/mccann_hub/odoolib/connector/_connector.py (1)
  • get_service (54-60)
🪛 Ruff (0.8.2)
src/mccann_hub/odoolib/connector/__init__.py

1-1: .json_rpc.JsonRpcConnector imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


1-1: .json_rpc.JsonRpcException imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


2-2: .json_rpcs.JsonRpcsConnector imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


3-3: .xml_rpc.XmlRpcConnector imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


4-4: .xml_rpcs.XmlRpcsConnector imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

src/mccann_hub/odoolib/connection/__init__.py

1-1: ._connection.Connection imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


2-2: .authentication_error.AuthenticationError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

src/mccann_hub/odoolib/model.py

102-103: Use a single if statement instead of nested if statements

(SIM102)

🪛 actionlint (1.7.4)
.github/workflows/test.yml

31-31: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 YAMLlint (1.35.1)
test_containers/compose.yaml

[error] 24-24: trailing spaces

(trailing-spaces)


[warning] 31-31: too many blank lines

(1 > 0) (empty-lines)

🪛 Shellcheck (0.10.0)
test_containers/postgres/docker-ensure-initdb.sh

[warning] 30-30: Expanding an array without an index only gives the first element.

(SC2128)

test_containers/postgres/docker-entrypoint.sh

[warning] 163-163: psql appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 177-177: ShellCheck can't follow non-constant source. Use a directive to specify location.

(SC1090)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 209-209: This assignment is only seen by the forked process.

(SC2097)


[warning] 209-209: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 209-209: This expansion will not see the mentioned assignment.

(SC2098)


[warning] 214-214: This assignment is only seen by the forked process.

(SC2097)


[warning] 214-214: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 214-214: This expansion will not see the mentioned assignment.

(SC2098)


[warning] 274-275: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 316-316: Expanding an array without an index only gives the first element.

(SC2128)

🔇 Additional comments (62)
src/mccann_hub/odoolib/connection/authentication_error.py (1)

34-39: LGTM: Good implementation of custom exception

The AuthenticationError class is well-defined with an appropriate docstring that clearly explains its purpose.

test_containers/config/odoo.conf (1)

5-8: Consider adding security restrictions for production use

The current configuration enables both XML-RPC and JSON-RPC interfaces on port 8069 without any IP restrictions. While this is acceptable for development or testing environments, you should consider adding security restrictions for production deployments.

Consider adding the following settings for production environments:

  • xmlrpc_interface and jsonrpc_interface to restrict which network interfaces Odoo listens on
  • proxy_mode if running behind a reverse proxy
  • dbfilter to restrict database access

Is this configuration intended for development/testing only, or will it be used in production?

requirements.txt (2)

2-2:

Details

✅ Verification successful

Verify the future date in certifi version

The certifi version is dated 2025.1.31, which appears to be a future date. This may cause installation issues as this version likely doesn't exist yet.


🌐 Web query:

What is the latest version of certifi package available?

💡 Result:

The latest version of the certifi Python package is 2025.1.31, released on January 30, 2025[1][7][8]. This version provides Mozilla's curated root certificate bundle for TLS/SSL validation and is available on PyPI[1].

For context:

  • Debian/Ubuntu systems have this version packaged as 2025.1.31+ds-1 (including Debian-specific modifications)[8].
  • Security vulnerabilities in earlier versions (like CVE-2024-39689 affecting versions before 2024.07.04) have been addressed in subsequent releases[3][4].

To install or upgrade:

pip install certifi --upgrade

The package continues to follow Mozilla's root certificate updates and maintains compatibility with Python deployments[1][9].

Citations:


Certifi Version Validated

The certifi version specified in requirements.txt (certifi==2025.1.31) is confirmed to be the latest release (January 30, 2025). There is no issue with this version, and no modifications are necessary.


5-6:

Details

❓ Verification inconclusive

Verify latest versions of dependent packages

Make sure the versions of requests and urllib3 are compatible with each other. Sometimes specific versions of urllib3 are required for certain requests versions.


🏁 Script executed:

#!/bin/bash
# Check if the specified versions are the latest available
echo "Checking latest versions of packages..."
pip index versions requests | head -n 5
pip index versions urllib3 | head -n 5

# Check if our specified versions are compatible
echo "Checking compatibility requirements..."
pip install requests==2.32.3 -v | grep -A 5 "Requires"

Length of output: 2482


Compatibility check for requests and urllib3 packages

The specified versions in requirements.txt (requests==2.32.3 and urllib3==2.3.0) are indeed the latest releases. However, the compatibility check using pip install requests==2.32.3 -v | grep -A 5 "Requires" did not output any dependency requirements. This makes it unclear whether the verbose output properly confirms that requests declares the necessary version—or range—of urllib3.

Please manually verify that the installed versions are compatible. For example, consider running additional checks (like pip check or reviewing requests’ installation metadata) to ensure no dependency conflicts exist.

src/mccann_hub/odoolib/dates.py (2)

72-74: Good formatting improvement for readability

The multi-line formatting of the strptime method call improves readability and makes the code more maintainable. This is a good change.


9-18: Consistent whitespace in comments

The changes to add blank lines within the license comment block improve readability. These formatting changes are good.

src/mccann_hub/odoolib/connector/xml_rpcs.py (1)

39-58: LGTM - Well implemented secure XML-RPC connector

The XmlRpcsConnector class properly extends XmlRpcConnector to provide secure HTTPS communication. It correctly overrides the PROTOCOL constant and the URL construction to use "https://" instead of "http://".

tests/test_xml_rpc.py (1)

35-66: Thorough test implementation for XmlRpcConnector

The test class provides good coverage of the connector functionality, including initialization and proper request handling.

test_containers/compose.yaml (1)

19-30:

Details

❓ Verification inconclusive

Verify the database image and health check configuration

The database service is using a custom image (ghcr.io/jabez007/odoo-client-lib) instead of a standard PostgreSQL image. Additionally, the health check's start period is set to 300s (5 minutes), which seems excessively long.


🏁 Script executed:

#!/bin/bash
# Check the custom image details
CUSTOM_IMAGE="ghcr.io/jabez007/odoo-client-lib"
echo "Checking image: $CUSTOM_IMAGE"
curl -s "https://api.github.com/users/jabez007/packages/container/odoo-client-lib/versions" | jq '.[0]'

# Check if this is a modified PostgreSQL image or something else
curl -s "https://api.github.com/repos/jabez007/odoo-client-lib/readme" | jq -r '.content' | base64 -d || echo "No README found"

Length of output: 554


Review: Verify Custom Image and Health Check Settings

  • The database service uses a custom image (ghcr.io/jabez007/odoo-client-lib). The GitHub API check did not yield clear metadata (JSON indexing error and no README), so please manually verify that this image is a modified PostgreSQL container and meets our requirements.
  • The health check’s start period is set to 300s, which may be excessively long. Confirm if this duration is necessary for proper initialization or if a shorter interval is feasible.
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 24-24: trailing spaces

(trailing-spaces)

src/mccann_hub/odoolib/connector/_sender.py (2)

33-43: Well-designed base class for RPC operations

The Sender class provides a solid foundation with a sensible stub implementation that can be overridden by child classes.


44-51: Well-implemented asynchronous wrapper

The async_send method correctly implements an asynchronous wrapper around the synchronous send method using run_in_executor, allowing for non-blocking execution while maintaining compatibility.

src/mccann_hub/odoolib/connector/_connector.py (4)

33-37: Well-structured imports with proper ordering

The imports are well-organized, with standard library imports (logging, typing) followed by relative imports from the package.


40-43: Good use of inheritance and clear documentation

The Connector class properly inherits from Sender and includes a descriptive docstring explaining its purpose.


45-45: Appropriate use of Optional typing

The PROTOCOL class variable is properly typed as Optional[str], allowing for None values and providing clear information about expected types.


54-60: Well-implemented get_service method

The get_service method properly creates and returns a Service instance, which aligns with the similar method seen in the _servicer.py file.

tests/test_json_rpc.py (4)

36-42: Good test class setup

The test class is well-organized with a proper setUp method that initializes the test object with relevant parameters.


43-50: Thorough initialization test

The test_initialization method properly verifies that all connector attributes are initialized correctly, including the URL, version, and logger name.


52-77: Comprehensive success path testing

The test_send_success method effectively tests the success path by:

  1. Mocking the external dependency (requests.post)
  2. Setting up the expected response
  3. Verifying that the correct data was sent in the request
  4. Checking that the response matches the expected result

79-88: Proper error path testing

The test_send_error method correctly tests the error handling by:

  1. Mocking an error response
  2. Verifying that the appropriate exception is raised
  3. Checking the exception message matches expectations
.github/workflows/test.yml (1)

13-20:

Details

✅ Verification successful

Consider Python version compatibility

The workflow tests against Python 3.13, which is not yet released. This might cause workflow failures until Python 3.13 is available.


🌐 Web query:

Is Python 3.13 released yet? What is the latest Python version available?

💡 Result:

Python 3.13 was officially released on October 7, 2024[3][4], after a brief delay from its original October 1 target due to performance issues with garbage collector changes[1][8]. The latest version available as of April 2025 is Python 3.13.2, released on February 4, 2025, which includes nearly 250 bug fixes and improvements over previous 3.13 releases[2][5][9].

Key details about Python 3.13:

  • Current stable version: 3.13.2 (maintenance release)
  • Support timeline: Security updates guaranteed until October 2029[6][7][9]
  • Major new features:
    • Experimental free-threaded mode (GIL-disabled builds)[1][3][4]
    • Preliminary JIT compiler groundwork[2][3][4]
    • Enhanced REPL with multi-line editing and color support[2][4][10]
    • Improved error messages with colorized tracebacks[3][10]

Python 3.13 marked the end of security support for Python 3.8[1][7], while development continues on Python 3.14 (currently in alpha)[5][7]. The release introduced significant under-the-hood changes to prepare for future performance enhancements, even though immediate speed improvements remain modest[3][10].

Citations:


Python 3.13 is now officially supported
Your workflow configuration in .github/workflows/test.yml is valid as-is. Python 3.13 was officially released in October 2024 (with the current stable release being 3.13.2 as of February 2025), so testing against it no longer poses compatibility risks.

tests/test_json_rpcs.py (1)

36-50: Test verifies secure HTTPS protocol usage

The test_initialization method correctly verifies that the JsonRpcsConnector uses HTTPS instead of HTTP, which is a good security practice for RPC connections.

src/mccann_hub/odoolib/main.py (1)

86-88: The get_connection implementation looks good.

The implementation properly uses the get_connector function to create a connector and passes all the necessary parameters to the Connection constructor.

src/mccann_hub/odoolib/service.py (2)

38-51: The Service class initialization looks good.

The Service class is well-designed with a clear initialization process that properly sets up logging for monitoring RPC calls.


76-78: Elegant implementation of async support.

Attaching the async_proxy directly to the proxy function as an attribute is an elegant design pattern that provides a clean API for both synchronous and asynchronous usage.

tests/test_async.py (1)

67-80: The test_search_count_async implementation is well-structured.

This test correctly validates that the count of search results matches the length of the results array, which is a good verification of the async functionality.

test_containers/postgres/Dockerfile (5)

1-5: This is a generated Dockerfile.

The comment indicates this file is generated via "apply-templates.sh" and should not be edited directly. Consider using the template generation process for any changes instead of directly modifying this file.


9-16: Well-configured PostgreSQL user setup.

The user and group setup follows best practices with explicit IDs and proper home directory permissions.


189-190: Consider the implications of removing the VOLUME directive.

The commented-out VOLUME directive is unconventional for PostgreSQL containers, which typically need persistent storage. This is explicitly done to enable database snapshots, but make sure you understand the implications for data persistence.

Is there a specific reason you're not using a volume for PostgreSQL data? This approach means data will be lost when the container is removed unless you're handling persistence elsewhere.


192-194:

Details

✅ Verification successful

Verify that the necessary entrypoint scripts exist.

The Dockerfile copies docker-entrypoint.sh and docker-ensure-initdb.sh into the container, but these files are not shown in the PR. Ensure these scripts exist and are properly configured.


🏁 Script executed:

#!/bin/bash
# Check if the referenced entrypoint scripts exist
ls -la test_containers/postgres/docker-entrypoint.sh test_containers/postgres/docker-ensure-initdb.sh

Length of output: 298


Entry Point Scripts Verified: Both necessary scripts are present and properly configured.

  • Verified that test_containers/postgres/docker-entrypoint.sh and test_containers/postgres/docker-ensure-initdb.sh exist in the repository with appropriate executable permissions.
  • No further action is required regarding these files.

89-173:

Details

❓ Verification inconclusive

Consider implications of using PostgreSQL 17.

PostgreSQL 17 is a very recent version and might not be compatible with applications designed for older versions. Verify that your Odoo installation is compatible with this PostgreSQL version.


🏁 Script executed:

#!/bin/bash
# Check if Odoo officially supports PostgreSQL 17
# Look for PostgreSQL version requirements in the documentation

# First, let's look for documentation files that might mention PostgreSQL
fd -t f -e md -e rst -e txt | xargs rg -l "postgres|PostgreSQL" | head -n 10

# Then, let's check specifically for version requirements
rg -i "postgres.*version|postgresql.*version|requires.*postgres" -A 3 -B 3

Length of output: 2058


Verify PostgreSQL 17 Compatibility for Odoo

The PostgreSQL 17 configuration in this Dockerfile is a recent update, and there isn’t any conclusive evidence from the documentation that Odoo fully supports this version. Please review the official Odoo compatibility guidelines to ensure:

  • All required extensions and dependencies are compatible with PostgreSQL 17.
  • The build and runtime behaviors (as seen from the Dockerfile configuration) don’t negatively impact your Odoo installation.
tests/test_connection.py (5)

36-45: LGTM! Good test setup

The test setup correctly mocks the connector and initializes the Connection with appropriate test parameters.


47-51: LGTM! Good initialization test

The test properly verifies that the Connection constructor correctly initializes all expected attributes.


53-62: LGTM! Good context testing technique

Great use of mocking to test the get_user_context method. The test verifies both the return value and that the expected methods are called with the correct parameters.


64-68: LGTM! Well-structured model test

The test properly verifies that get_model returns an instance of the correct type with the expected attributes.


71-72: LGTM! Standard unittest entry point

Good practice to include the standard unittest entry point for running tests individually.

src/mccann_hub/odoolib/connection/_connection.py (2)

40-67: LGTM! Well-documented Connection class

The Connection class is well-structured with clear documentation. It correctly inherits from Servicer and initializes the user_context attribute.


84-90: LGTM! Well-implemented get_model method

The get_model method correctly creates and returns a Model instance with the appropriate parameters.

tests/test_xml_rpcs.py (2)

35-50: LGTM! Good test structure and initialization test

The test class is well-structured with appropriate setup and initialization testing. The assertions verify the URL construction, logger name, and transport initialization.


69-70: LGTM! Standard unittest entry point

Good practice to include the standard unittest entry point for running tests individually.

tests/test_model.py (5)

35-42: LGTM! Comprehensive test setup

The test setup thoroughly configures the mock objects and initializes the Model with appropriate test parameters.


44-47: LGTM! Good logger name verification

The test correctly verifies that the logger name is properly constructed.


49-67: LGTM! Thorough proxy method tests

The tests for proxy method calls cover important scenarios: single ID retrieval, multiple ID retrieval, and handling non-existent IDs. The assertions and mock configurations are appropriate.


69-78: LGTM! Well-structured search_read tests

The tests for search_read properly verify both the case where results are found and the case where no results are found.


80-82: LGTM! Standard unittest entry point

Good practice to include the standard unittest entry point for running tests individually.

.gitignore (1)

1-175: Comprehensive and well-structured .gitignore file

This .gitignore file is complete and follows standard Python project practices, covering all necessary exclusions like byte-compiled files, packages, testing artifacts, and environment-specific files.

Some configuration options are commented out that you might consider uncommenting based on your project's needs:

  • Line 168: .idea/ for PyCharm/IntelliJ
  • Lines 95-108: Various lock files for dependency management tools (Pipfile.lock, poetry.lock)

These are typically project decisions based on whether you want consistent dependency versions checked into your repository.

tests/test.py (2)

40-41: Namespace package import is a good improvement

Updating the import from import odoolib to import mccann_hub.odoolib as odoolib is a positive change that follows better namespace package practices.


49-55: Connection method refactoring improves readability

The reformatted _conn method is more readable with proper indentation and parameter alignment.

src/mccann_hub/odoolib/connector/json_rpc.py (2)

41-47: Custom exception provides good error handling foundation

The JsonRpcException class is well-defined and allows for clear error propagation from JSON-RPC calls.


71-88:

Details

✅ Verification successful

Improve error handling in _json_rpc method

While the method handles JSON-RPC errors, it doesn't handle HTTP request failures:

    def _json_rpc(self, fct_name, params):
+       """
+       Execute a JSON-RPC call with the given function name and parameters.
+       
+       :param fct_name: The RPC function to call
+       :param params: The parameters to pass to the function
+       :return: The result of the RPC call
+       :raises: JsonRpcException if the server returns an error
+       :raises: requests.RequestException if the HTTP request fails
+       """
        data = {
            "jsonrpc": self.version,
            "method": fct_name,
            "params": params,
            "id": random.randint(0, 1000000000),
        }
-       result_req = requests.post(
-           self.url,
-           data=json.dumps(data),
-           headers={
-               "Content-Type": "application/json",
-           },
-       )
-       result = result_req.json()
+       try:
+           result_req = requests.post(
+               self.url,
+               data=json.dumps(data),
+               headers={
+                   "Content-Type": "application/json",
+               },
+               timeout=60,  # Add a reasonable timeout
+           )
+           result_req.raise_for_status()  # Raise exception for 4XX/5XX responses
+           result = result_req.json()
+       except requests.RequestException as e:
+           # Handle network or HTTP errors
+           raise JsonRpcException({"code": -32603, "message": f"Transport error: {str(e)}"})
+       except json.JSONDecodeError:
+           raise JsonRpcException({"code": -32700, "message": "Invalid JSON response"})
        if result.get("error", None):
            raise JsonRpcException(result["error"])
        return result.get("result", False)

Also, consider using secrets instead of random for generating the request ID in security-sensitive applications:

-           "id": random.randint(0, 1000000000),
+           "id": secrets.randbelow(1000000000),

🏁 Script executed:

#!/bin/bash
# Check whether the request ID generation is used in a secure context

# Look for any authentication or sensitive data processing in the codebase
rg -A 5 -B 5 "login|password|token|auth|credential" --type py

Length of output: 21445


Refactor _json_rpc Method for Robust Error Handling and Secure ID Generation

Please update the _json_rpc method in src/mccann_hub/odoolib/connector/json_rpc.py (lines 71–88) as follows:

  • Add a concise docstring describing the parameters, return values, and raised exceptions.
  • Wrap the HTTP request in a try/except block:
    • Call raise_for_status() on the response to catch HTTP errors,
    • Catch requests.RequestException for transport-level issues and wrap them in a JsonRpcException,
    • Catch json.JSONDecodeError to handle invalid JSON responses.
  • Specify a reasonable timeout (e.g., 60 seconds) for the HTTP request.
  • Replace the use of random.randint with secrets.randbelow for generating the request ID in security-sensitive scenarios, given the presence of authentication details elsewhere in the codebase.

Suggested diff snippet:

    def _json_rpc(self, fct_name, params):
+       """
+       Execute a JSON-RPC call with the given function name and parameters.
+       
+       :param fct_name: The RPC function to call
+       :param params: The parameters to pass to the function
+       :return: The result of the RPC call
+       :raises: JsonRpcException if the server returns an error or transport issue occurs
+       """
        data = {
-           "id": random.randint(0, 1000000000),
+           "id": secrets.randbelow(1000000000),
            "jsonrpc": self.version,
            "method": fct_name,
            "params": params,
        }
-       result_req = requests.post(
-           self.url,
-           data=json.dumps(data),
-           headers={
-               "Content-Type": "application/json",
-           },
-       )
-       result = result_req.json()
+       try:
+           result_req = requests.post(
+               self.url,
+               data=json.dumps(data),
+               headers={"Content-Type": "application/json"},
+               timeout=60,
+           )
+           result_req.raise_for_status()
+           result = result_req.json()
+       except requests.RequestException as e:
+           raise JsonRpcException({"code": -32603, "message": f"Transport error: {str(e)}"})
+       except json.JSONDecodeError:
+           raise JsonRpcException({"code": -32700, "message": "Invalid JSON response"})
        if result.get("error", None):
            raise JsonRpcException(result["error"])
        return result.get("result", False)

This refactor improves the resilience of the JSON-RPC call by handling network, HTTP, and JSON parsing errors robustly, while also enhancing security for request ID generation.

tests/test_service.py (4)

1-34: No issues found in license headers and imports.


35-43: TestService setup and initialization.
The class and logger validation test are clear and straightforward.


44-53: Synchronous method call test is well-structured.
The mock usage and assertion logic look correct.


64-68: Method name reflection test is valid.
Verifying the existence of .async_ is a solid check.

test_containers/postgres/docker-ensure-initdb.sh (2)

1-29: Initial script and environment setup look fine.
No issues found regarding environment configuration or Docker entrypoint usage in these lines.


31-72: No additional issues identified.
Overall, the script handles initialization logic cleanly.

src/mccann_hub/odoolib/model.py (4)

1-52: File setup, imports, constructor, and logging approach look good.


53-96: Dynamic method generation and async proxy are well-structured.
The approach for synchronous and asynchronous calls is consistent and aligns well with Odoo’s dynamic RPC style.


116-137: The search_read method is concise and follows Odoo conventions.


138-161: The asynchronous search_read flow is consistent and correct.
No concerns regarding the async logic.

src/mccann_hub/odoolib/connection/_servicer.py (1)

41-140: LGTM! The Servicer class is well-structured with clear separation of concerns.

The class provides both synchronous and asynchronous authentication methods, which is good practice for modern Python applications. The error handling is appropriate and the logging will be helpful for debugging.

setup.py (1)

35-60: The setup configuration looks good!

The package configuration has been properly updated to reflect the new namespace structure and version. The classifiers and metadata are accurately specified.

test_containers/postgres/docker-entrypoint.sh (1)

1-360: This looks like a standard PostgreSQL Docker entrypoint script.

The script follows common patterns for initializing a PostgreSQL database in a Docker container. It handles environment variables, database initialization, and user permissions appropriately.

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 163-163: psql appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 177-177: ShellCheck can't follow non-constant source. Use a directive to specify location.

(SC1090)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 209-209: This assignment is only seen by the forked process.

(SC2097)


[warning] 209-209: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 209-209: This expansion will not see the mentioned assignment.

(SC2098)


[warning] 214-214: This assignment is only seen by the forked process.

(SC2097)


[warning] 214-214: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 214-214: This expansion will not see the mentioned assignment.

(SC2098)


[warning] 274-275: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 316-316: Expanding an array without an index only gives the first element.

(SC2128)

Comment on lines +52 to +66
@patch("mccann_hub.odoolib.connector.xml_rpc.ServerProxy")
def test_send(self, mock_server_proxy):
mock_service = MagicMock()
mock_method = MagicMock(return_value="mock_response")
mock_service.some_method = mock_method
mock_server_proxy.return_value = mock_service

response = self.connector.send("common", "some_method", "arg1", "arg2")

mock_server_proxy.assert_called_once_with(
f"https://{self.hostname}:{self.port}/xmlrpc/{self.version}/common",
transport=None,
)
mock_method.assert_called_once_with("arg1", "arg2")
self.assertEqual(response, "mock_response")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Missing error handling test

The test for send covers the success case, but there's no test for error handling. Based on the method's expected behavior, there should be tests for error conditions.

Add a test method for error handling:

@patch("mccann_hub.odoolib.connector.xml_rpc.ServerProxy")
def test_send_error(self, mock_server_proxy):
    mock_service = MagicMock()
    mock_method = MagicMock(side_effect=Exception("Test error"))
    mock_service.some_method = mock_method
    mock_server_proxy.return_value = mock_service

    with self.assertRaises(Exception):
        self.connector.send("common", "some_method", "arg1", "arg2")

    mock_server_proxy.assert_called_once_with(
        f"https://{self.hostname}:{self.port}/xmlrpc/{self.version}/common",
        transport=None,
    )
    mock_method.assert_called_once_with("arg1", "arg2")

Comment on lines +35 to +78
class TestModel(unittest.TestCase):
def setUp(self):
self.mock_connection = MagicMock()
self.mock_connection.database = "test_db"
self.mock_connection.user_id = 1
self.mock_connection.password = "pass"
self.mock_connection.get_service.return_value.execute_kw = MagicMock()
self.model = Model(self.mock_connection, "res.partner")

def test_initialization(self):
self.assertEqual(
self.model._logger.name, "mccann_hub.odoolib.model.res.partner"
)

def test_proxy_method_call(self):
self.mock_connection.get_service.return_value.execute_kw.return_value = [
{"id": 1, "name": "John"}
]
result = self.model.read([1])
self.assertEqual(result, [{"id": 1, "name": "John"}])

def test_proxy_method_call_list_ids(self):
self.mock_connection.get_service.return_value.execute_kw.return_value = [
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"},
]
result = self.model.read([1, 2])
self.assertEqual(result, [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}])

def test_proxy_method_call_id_not_found(self):
self.mock_connection.get_service.return_value.execute_kw.return_value = []
result = self.model.read([999])
self.assertFalse(result)

def test_search_read(self):
self.model.search = MagicMock(return_value=[1])
self.model.read = MagicMock(return_value=[{"id": 1, "name": "X"}])
result = self.model.search_read([("is_company", "=", True)], ["name"])
self.assertEqual(result, [{"id": 1, "name": "X"}])

def test_search_read_no_result(self):
self.model.search = MagicMock(return_value=[])
result = self.model.search_read([("is_company", "=", False)])
self.assertEqual(result, [])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Missing async method tests

The Model class supports both synchronous and asynchronous operations, but the tests only cover the synchronous methods. Add tests for the asynchronous methods to ensure complete coverage.

Here's an example of how to test an async method:

@patch('asyncio.run')
def test_async_proxy_method_call(self, mock_run):
    # Setup mock response
    async_result = [{"id": 1, "name": "John"}]
    mock_run.return_value = async_result
    
    # Create a coroutine mock
    mock_coro = MagicMock()
    mock_coro.__await__ = lambda self: async_result.__await__()
    
    # Setup the async_ method to return our coroutine mock
    self.model.read.async_ = MagicMock(return_value=mock_coro)
    
    # Run the test
    result = asyncio.run(self.model.read.async_([1]))
    self.assertEqual(result, [{"id": 1, "name": "John"}])
    
    # Verify the async method was called
    self.model.read.async_.assert_called_once_with([1])

Consider adding similar tests for other async methods like search.async_ and search_read.async_ if they exist.

Comment on lines +66 to +69
def send(self, service_name: str, method: str, *args):
url = "%s/%s" % (self.url, service_name)
service = ServerProxy(url, transport=self._transport)
return getattr(service, method)(*args)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and documentation to send method

The send method lacks error handling for connection issues or remote exceptions, and is missing return type annotation and documentation.

-    def send(self, service_name: str, method: str, *args):
+    def send(self, service_name: str, method: str, *args) -> any:
+        """
+        Send a request to the specified service and method with the given arguments.
+        
+        :param service_name: The name of the service to call (e.g., 'common', 'object', etc.)
+        :param method: The method name to call on the service
+        :param args: Additional arguments to pass to the method
+        :return: The result of the method call
+        :raises: xmlrpc.client.Fault if the remote call fails
+        """
         url = "%s/%s" % (self.url, service_name)
-        service = ServerProxy(url, transport=self._transport)
-        return getattr(service, method)(*args)
+        service = ServerProxy(url, transport=self._transport)
+        try:
+            return getattr(service, method)(*args)
+        except Exception as e:
+            # Log the error or transform it to a more specific exception
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def send(self, service_name: str, method: str, *args):
url = "%s/%s" % (self.url, service_name)
service = ServerProxy(url, transport=self._transport)
return getattr(service, method)(*args)
def send(self, service_name: str, method: str, *args) -> any:
"""
Send a request to the specified service and method with the given arguments.
:param service_name: The name of the service to call (e.g., 'common', 'object', etc.)
:param method: The method name to call on the service
:param args: Additional arguments to pass to the method
:return: The result of the method call
:raises: xmlrpc.client.Fault if the remote call fails
"""
url = "%s/%s" % (self.url, service_name)
service = ServerProxy(url, transport=self._transport)
try:
return getattr(service, method)(*args)
except Exception as e:
# Log the error or transform it to a more specific exception
raise

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (12)
test_containers/config/odoo.conf (1)

4-4: Avoid hardcoding the admin password

Hardcoding passwords in configuration files is a security risk, especially when those files might be committed to version control. Consider using environment variables or a secrets management system instead.

- admin_passwd = P@$$w0rd
+ admin_passwd = ${ODOO_ADMIN_PASSWORD}

Then provide the password via environment variable in your deployment process.

requirements.txt (1)

1-1: asyncio package is unnecessary

The asyncio package is part of the Python standard library since Python 3.4 and shouldn't be explicitly installed. It's already available in any Python 3.4+ environment without requiring a separate package installation.

-asyncio==3.4.3
test_containers/compose.yaml (1)

22-24: Avoid hardcoded credentials in configuration files

The database credentials are hardcoded in the configuration. Consider using environment variables or secrets management.

  environment:
    - POSTGRES_DB=postgres
-    - POSTGRES_USER=odoo
-    - POSTGRES_PASSWORD=odoo 
+    - POSTGRES_USER=${POSTGRES_USER:-odoo}
+    - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-odoo}
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 24-24: trailing spaces

(trailing-spaces)

.github/workflows/test.yml (1)

30-33: Update GitHub Actions setup-python version

The static analysis hints are correct—using actions/setup-python@v4 is outdated. The recommended version is now v5.

- name: Setup Python
- uses: actions/setup-python@v4
+ name: Setup Python
+ uses: actions/setup-python@v5
  with:
    python-version: ${{ matrix.python_version }}
🧰 Tools
🪛 actionlint (1.7.4)

31-31: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

tests/test_xml_rpcs.py (1)

52-66: 🛠️ Refactor suggestion

Missing error handling test

The test for send covers the success case, but there's no test for error handling. Based on the method's expected behavior, there should be tests for error conditions.

Add a test method for error handling:

@patch("mccann_hub.odoolib.connector.xml_rpc.ServerProxy")
def test_send_error(self, mock_server_proxy):
    mock_service = MagicMock()
    mock_method = MagicMock(side_effect=Exception("Test error"))
    mock_service.some_method = mock_method
    mock_server_proxy.return_value = mock_service

    with self.assertRaises(Exception):
        self.connector.send("common", "some_method", "arg1", "arg2")

    mock_server_proxy.assert_called_once_with(
        f"https://{self.hostname}:{self.port}/xmlrpc/{self.version}/common",
        transport=None,
    )
    mock_method.assert_called_once_with("arg1", "arg2")
tests/test_model.py (1)

44-78: 🛠️ Refactor suggestion

Missing async method tests for the Model class.

The test suite thoroughly tests synchronous methods but is missing tests for the asynchronous methods available in the Model class.

Here's an example of how to test an async method:

import asyncio
from unittest.mock import MagicMock, patch

def test_async_proxy_method_call(self):
    # Setup mock response
    async_result = [{"id": 1, "name": "John"}]
    
    # Mock the async execute_kw call
    mock_execute_kw_async = MagicMock()
    async def mock_async(*args, **kwargs):
        return async_result
    mock_execute_kw_async.async_ = mock_async
    self.mock_connection.get_service.return_value.execute_kw = mock_execute_kw_async
    
    # Run the test with asyncio.run
    result = asyncio.run(self.model.read.async_([1]))
    
    # Verify results
    self.assertEqual(result, [{"id": 1, "name": "John"}])

Consider adding similar tests for other async methods like search.async_ and search_read.async_.

src/mccann_hub/odoolib/connector/xml_rpc.py (1)

66-78: 🛠️ Refactor suggestion

Add error handling to the send method.

While the method documentation has been added as suggested in the previous review, there's still no explicit error handling for connection issues or remote exceptions.

def send(self, service_name: str, method: str, *args):
    """
    Send a request to the specified service and method with the given arguments.

    :param service_name: The name of the service to call (e.g., 'common', 'object', etc.)
    :param method: The method name to call on the service
    :param args: Additional arguments to pass to the method
    :return: The result of the method call
    :raises: xmlrpc.client.Fault if the remote call fails
    """
    url = "%s/%s" % (self.url, service_name)
-   service = ServerProxy(url, transport=self._transport)
-   return getattr(service, method)(*args)
+   service = ServerProxy(url, transport=self._transport)
+   try:
+       return getattr(service, method)(*args)
+   except Exception as e:
+       self._logger.error("XML-RPC call failed: %s", str(e))
+       raise
src/mccann_hub/odoolib/connection/_connection.py (1)

68-82: (duplicate) Potential race on user_context cache

The cached user_context is written by both sync and async getters with no locking, which can lead to inconsistent state under concurrent access. Although earlier you noted that callers should use either sync or async APIs, multithreaded sync use cases (or concurrent await scheduling) can still interleave.

Re-consider adding a (re-entrant) lock or an immutable snapshot to guarantee safety.

pyproject.toml (1)

31-34: Remove asyncio from dependencies (standard library)

asyncio ships with Python ≥ 3.4 and must not be declared as an external requirement. Keeping it confuses tooling and users:

 dependencies = [
     "requests",
-    "asyncio",
 ]

No functional change, but cleans up packaging metadata.

src/mccann_hub/odoolib/connection/_servicer.py (2)

104-110: Address the TODO comment about using authenticate instead of login.

There's a TODO comment about using authenticate instead of login. This needs to be implemented or the TODO should be documented with a rationale for keeping the current approach.

This issue was previously identified in a review comment. Either implement the authenticate method in the common service, document why login is preferred, or remove the TODO comment if it's not planned.


123-129: Address the TODO comment in the async implementation as well.

The same TODO comment about using authenticate instead of login appears in the asynchronous implementation. Both instances should be addressed consistently.

This is related to the same issue flagged earlier in the synchronous implementation. If you update the synchronous method, make sure to update the asynchronous one as well.

setup.py (1)

46-48: 🛠️ Refactor suggestion

Remove unnecessary dependency on built-in module.

asyncio is a built-in module in Python 3.4+ and should not be listed as an external dependency since your package already requires Python 3.9+ (as specified in python_requires).

    install_requires=[
        "requests",
-        "asyncio",
    ],
🧹 Nitpick comments (26)
src/mccann_hub/odoolib/connection/authentication_error.py (1)

8-8: Future copyright date

The copyright year 2025 is in the future. Consider updating to the current year or using a range (e.g., 2024-2025) if this is intentional.

- # Copyright (C) 2025 Jimmy McCann
+ # Copyright (C) 2024 Jimmy McCann
src/mccann_hub/odoolib/connector/json_rpcs.py (1)

36-40: Update docstring to specify secure protocol

The current docstring doesn't indicate that this connector uses the secure JsonRPC protocol over HTTPS, which is the key difference from its parent class.

- A type of connector that uses the JsonRPC protocol.
+ A type of connector that uses the secured JsonRPC protocol over HTTPS.
tests/test_xml_rpc.py (2)

4-4: Update the copyright year

The copyright year 2025 is in the future. Consider updating it to the current year (2024) for accuracy.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann

40-40: Consider initializing with explicit parameters for better test coverage

The connector is initialized with only the hostname, relying on default values for port and version. For better test coverage, consider explicitly passing all parameters.

-        self.connector = XmlRpcConnector(self.hostname)
+        self.connector = XmlRpcConnector(self.hostname, self.port, self.version)
test_containers/compose.yaml (2)

13-17: Consider making healthcheck more robust

The current healthcheck relies on curl being available in the container. For improved reliability, consider using a more robust approach or ensuring curl is installed.

   healthcheck:
-      test: curl --fail http://localhost:8069/web/login || exit 1
+      test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8069/web/login"]
       start_period: 10s
       interval: 10s
       timeout: 5s
       retries: 5

26-30: Reduce health check start period

The current start period of 300s (5 minutes) for the database health check seems excessive and could significantly delay startup in testing environments.

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
-      start_period: 300s
+      start_period: 60s
      interval: 10s
      timeout: 5s
      retries: 5
src/mccann_hub/odoolib/connector/_sender.py (1)

4-4: Update the copyright year

The copyright year 2025 is in the future. Consider updating it to the current year (2024) for accuracy.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
.github/workflows/publish.yml (3)

15-19: Consider specifying a more precise Python version

Using "3.x" will use the latest Python 3 version, which might lead to unexpected behavior with future Python releases. Consider specifying a more precise version range.

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
-          python-version: "3.x"
+          python-version: "3.9"

20-27: Add caching for pip dependencies

Adding a caching step for pip dependencies can significantly improve workflow execution time by avoiding repeated downloads.

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.x"

+      - name: Cache pip dependencies
+        uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-

      - name: Install build tools
        run: |
          python -m pip install --upgrade build

76-78: Add pip upgrade and consider pinning version

It's a good practice to ensure pip is updated before installing packages. Also, for verification purposes, consider pinning the package version.

      - name: Install package from PyPI
        run: |
-          python -m pip install mccann_hub-odoo_client_lib
+          python -m pip install --upgrade pip
+          python -m pip install mccann_hub-odoo_client_lib==${{ github.event.release.tag_name }}
tests/test_json_rpc.py (1)

79-88: Consider adding test for async_send method.

While the current tests cover the synchronous send method well, there's no coverage for the asynchronous async_send method inherited from the Sender class.

+ @patch("mccann_hub.odoolib.connector.json_rpc.requests.post")
+ async def test_async_send_success(self, mock_post):
+     mock_response = MagicMock()
+     mock_response.json.return_value = {"result": "mock_response"}
+     mock_post.return_value = mock_response
+
+     response = await self.connector.async_send("common", "some_method", "arg1", "arg2")
+
+     _, called_kwargs = mock_post.call_args
+     sent_data = json.loads(called_kwargs["data"])
+
+     self.assertEqual(response, "mock_response")
.github/workflows/test-publish.yml (2)

19-20: Consider specifying a more precise Python version.

Using "3.x" will pull the latest Python 3 version, which might lead to unexpected behavior if new Python versions introduce breaking changes. Consider specifying a specific version range that matches your compatibility requirements.

- python-version: "3.x"
+ python-version: "3.8 - 3.11"

69-70: Consider validating more functionality in the verification step.

The current verification only checks if the package can be imported. Consider adding a basic smoke test that exercises core functionality to ensure the published package works as expected.

- python -c "import mccann_hub.odoolib; print('✅ Import successful')"
+ python -c "import mccann_hub.odoolib as odoolib; connector = odoolib.get_connector('localhost', 'jsonrpc'); print('✅ Import and basic functionality successful')"
src/mccann_hub/odoolib/connector/_connector.py (1)

47-52: Add constructor docstring and enforce URL initialization.

The constructor lacks a docstring explaining its purpose, and the url attribute is initialized to None but each subclass is expected to set it to a valid value. Consider adding a docstring and requiring subclasses to set the URL.

 def __init__(self):
+    """
+    Initialize the connector with a logger.
+    Subclasses must set the `url` attribute to a valid URL string.
+    """
     self._logger = logging.getLogger(
         f"{str.join('.', __name__.split('.')[:-1])}"
         + (f".{self.PROTOCOL}" if self.PROTOCOL is not None else "")
     )
     self.url: Optional[str] = None
src/mccann_hub/odoolib/main.py (2)

46-51: Update docstring to reflect all supported protocols.

The docstring only mentions XMLRPC, but the function supports xmlrpc, xmlrpcs, jsonrpc, and jsonrpcs protocols.

 """
-A shortcut method to easily create a connector to a remote server using XMLRPC.
+A shortcut method to easily create a connector to a remote server using various protocols.

 :param hostname: The hostname to the remote server.
 :param protocol: The name of the protocol, must be "xmlrpc", "xmlrpcs", "jsonrpc" or "jsonrpcs".
 :param port: The number of the port. Defaults to 8069.
 """

79-84: Remove non-existent parameter from docstring.

The docstring includes a connector parameter that doesn't exist in the function signature.

 :param protocol: The name of the protocol, must be "xmlrpc", "xmlrpcs", "jsonrpc" or "jsonrpcs".
 :param port: The number of the port. Defaults to 8069.
-:param connector: A valid Connector instance to send messages to the remote server.
 :param database: The name of the database to work on.
 :param login: The login of the user.
 :param password: The password of the user.
tests/test.py (1)

8-8: Future copyright date needs revision.

The copyright year is set to 2025, which is in the future. Consider updating it to the current year (2024) to accurately reflect when the code was written.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
src/mccann_hub/odoolib/service.py (1)

8-8: Future copyright date needs revision.

The copyright year is set to 2025, which is in the future. Consider updating it to the current year (2024) to accurately reflect when the code was written.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
tests/test_connection.py (1)

4-4: Future copyright date needs revision.

The copyright year is set to 2025, which is in the future. Consider updating it to the current year (2024) to accurately reflect when the code was written.

-# Copyright (C) 2025 Jimmy McCann
+# Copyright (C) 2024 Jimmy McCann
test_containers/postgres/docker-ensure-initdb.sh (1)

28-31: Consider handling shell safety in user switching.

The shellcheck static analysis flagged line 30 with SC2128 about expanding an array without an index. While this is actually a false positive for $@ which correctly expands to all arguments, it's worth double-checking your shell script practices.

-	exec gosu postgres "$BASH_SOURCE" "$@"
+	exec gosu postgres "$BASH_SOURCE" "$@"
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 30-30: Expanding an array without an index only gives the first element.

(SC2128)

src/mccann_hub/odoolib/connector/xml_rpc.py (1)

33-36: Consider more precise typing imports.

The import for Optional from typing is good, but you might also want to add Any for the return type of the send method to be more explicit about the type.

-from typing import Optional
+from typing import Any, Optional
src/mccann_hub/odoolib/connector/json_rpc.py (2)

66-70: Consider supporting keyword parameters

send() currently forwards only positional *args. Some Odoo endpoints accept keyword arguments (e.g. context={...}) that callers may want to pass explicitly:

-    def send(self, service_name: str, method: str, *args):
-        return self._json_rpc(
-            "call", {"service": service_name, "method": method, "args": args}
-        )
+    def send(self, service_name: str, method: str, *args, **kwargs):
+        return self._json_rpc(
+            "call",
+            {
+                "service": service_name,
+                "method": method,
+                "args": args,
+                "kwargs": kwargs,
+            },
+        )

The server will ignore kwargs if unused, while enabling richer calls when needed.


76-77: Use uuid for request IDs

random.randint(…) can collide across processes and offers no semantic meaning. Using uuid.uuid4().hex (or secrets.randbits) eliminates that risk and communicates intent:

-            "id": random.randint(0, 1000000000),
+            "id": uuid.uuid4().hex,

Since the value is opaque to the server, changing type to str is safe.

src/mccann_hub/odoolib/model.py (1)

102-113: Simplify nested conditionals & leverage dict-comprehension

Ruff flag SIM102 indicates the nested if chain can be flattened, improving readability and performance slightly:

-        if method == "read":
-            if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
-                index = {}
-                for r in result:
-                    index[r["id"]] = r
-                if isinstance(args[0], list):
-                    result = [index[x] for x in args[0] if x in index]
-                elif args[0] in index:
-                    result = index[args[0]]
-                else:
-                    result = False
+        if (
+            method == "read"
+            and isinstance(result, list)
+            and result
+            and "id" in result[0]
+        ):
+            index = {r["id"]: r for r in result}
+            key = args[0]
+            if isinstance(key, list):
+                result = [index[x] for x in key if x in index]
+            else:
+                result = index.get(key, False)

Same behaviour, fewer branches, clearer intent.

🧰 Tools
🪛 Ruff (0.8.2)

102-103: Use a single if statement instead of nested if statements

(SIM102)

test_containers/postgres/docker-entrypoint.sh (2)

201-201: Fix syntax in variable assignments.

There are spaces after equal signs in variable assignments that should be removed according to Shellcheck. In Bash, spaces around = in variable assignments can cause unexpected behavior.

- PGHOST= PGHOSTADDR= "${query_runner[@]}" "$@"
+ PGHOST="" PGHOSTADDR="" "${query_runner[@]}" "$@"

- POSTGRES_DB= docker_process_sql --dbname postgres --set db="$POSTGRES_DB" --tuples-only <<-'EOSQL'
+ POSTGRES_DB="" docker_process_sql --dbname postgres --set db="$POSTGRES_DB" --tuples-only <<-'EOSQL'

- POSTGRES_DB= docker_process_sql --dbname postgres --set db="$POSTGRES_DB" <<-'EOSQL'
+ POSTGRES_DB="" docker_process_sql --dbname postgres --set db="$POSTGRES_DB" <<-'EOSQL'

- NOTIFY_SOCKET= \
+ NOTIFY_SOCKET="" \

Also applies to: 209-209, 214-214, 274-275

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


316-316: Array expansion without index warning.

Shellcheck warns about expanding $BASH_SOURCE without an index. In Bash, expanding an array without an index only gives the first element. If this is intentional, consider making it explicit.

- exec gosu postgres "$BASH_SOURCE" "$@"
+ exec gosu postgres "$BASH_SOURCE[0]" "$@"

However, if $BASH_SOURCE is not intended to be an array in this context, you can ignore this warning.

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 316-316: Expanding an array without an index only gives the first element.

(SC2128)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25b15ea and 72b8fb8.

📒 Files selected for processing (38)
  • .github/workflows/publish.yml (1 hunks)
  • .github/workflows/test-publish.yml (1 hunks)
  • .github/workflows/test.yml (1 hunks)
  • .gitignore (1 hunks)
  • README.rst (2 hunks)
  • odoolib/main.py (0 hunks)
  • pyproject.toml (1 hunks)
  • requirements.txt (1 hunks)
  • setup.py (2 hunks)
  • src/mccann_hub/odoolib/connection/__init__.py (1 hunks)
  • src/mccann_hub/odoolib/connection/_connection.py (1 hunks)
  • src/mccann_hub/odoolib/connection/_servicer.py (1 hunks)
  • src/mccann_hub/odoolib/connection/authentication_error.py (1 hunks)
  • src/mccann_hub/odoolib/connector/__init__.py (1 hunks)
  • src/mccann_hub/odoolib/connector/_connector.py (1 hunks)
  • src/mccann_hub/odoolib/connector/_sender.py (1 hunks)
  • src/mccann_hub/odoolib/connector/json_rpc.py (1 hunks)
  • src/mccann_hub/odoolib/connector/json_rpcs.py (1 hunks)
  • src/mccann_hub/odoolib/connector/xml_rpc.py (1 hunks)
  • src/mccann_hub/odoolib/connector/xml_rpcs.py (1 hunks)
  • src/mccann_hub/odoolib/dates.py (5 hunks)
  • src/mccann_hub/odoolib/main.py (1 hunks)
  • src/mccann_hub/odoolib/model.py (1 hunks)
  • src/mccann_hub/odoolib/service.py (1 hunks)
  • test_containers/compose.yaml (1 hunks)
  • test_containers/config/odoo.conf (1 hunks)
  • test_containers/postgres/Dockerfile (1 hunks)
  • test_containers/postgres/docker-ensure-initdb.sh (1 hunks)
  • test_containers/postgres/docker-entrypoint.sh (1 hunks)
  • tests/test.py (2 hunks)
  • tests/test_async.py (1 hunks)
  • tests/test_connection.py (1 hunks)
  • tests/test_json_rpc.py (1 hunks)
  • tests/test_json_rpcs.py (1 hunks)
  • tests/test_model.py (1 hunks)
  • tests/test_service.py (1 hunks)
  • tests/test_xml_rpc.py (1 hunks)
  • tests/test_xml_rpcs.py (1 hunks)
💤 Files with no reviewable changes (1)
  • odoolib/main.py
🧰 Additional context used
🧬 Code Graph Analysis (15)
src/mccann_hub/odoolib/main.py (5)
src/mccann_hub/odoolib/connection/_connection.py (1)
  • Connection (40-90)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • JsonRpcConnector (49-88)
src/mccann_hub/odoolib/connector/json_rpcs.py (1)
  • JsonRpcsConnector (36-50)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • XmlRpcConnector (39-78)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
  • XmlRpcsConnector (39-58)
tests/test_json_rpc.py (1)
src/mccann_hub/odoolib/connector/json_rpc.py (3)
  • JsonRpcConnector (49-88)
  • JsonRpcException (41-46)
  • send (66-69)
src/mccann_hub/odoolib/connector/json_rpcs.py (1)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • JsonRpcConnector (49-88)
tests/test_xml_rpc.py (1)
src/mccann_hub/odoolib/connector/xml_rpc.py (2)
  • XmlRpcConnector (39-78)
  • send (66-78)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • XmlRpcConnector (39-78)
tests/test_model.py (1)
src/mccann_hub/odoolib/model.py (2)
  • Model (38-160)
  • search_read (116-136)
src/mccann_hub/odoolib/connection/__init__.py (2)
src/mccann_hub/odoolib/connection/_connection.py (1)
  • Connection (40-90)
src/mccann_hub/odoolib/connection/authentication_error.py (1)
  • AuthenticationError (34-39)
src/mccann_hub/odoolib/connector/_sender.py (2)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • send (66-78)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • send (66-69)
src/mccann_hub/odoolib/connector/_connector.py (3)
src/mccann_hub/odoolib/service.py (1)
  • Service (38-78)
src/mccann_hub/odoolib/connector/_sender.py (1)
  • Sender (33-51)
src/mccann_hub/odoolib/connection/_servicer.py (1)
  • get_service (131-139)
tests/test_xml_rpcs.py (3)
src/mccann_hub/odoolib/connector/xml_rpcs.py (1)
  • XmlRpcsConnector (39-58)
tests/test_xml_rpc.py (3)
  • setUp (36-40)
  • test_initialization (42-50)
  • test_send (53-66)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • send (66-78)
src/mccann_hub/odoolib/service.py (3)
src/mccann_hub/odoolib/connector/_sender.py (3)
  • Sender (33-51)
  • send (38-42)
  • async_send (44-51)
src/mccann_hub/odoolib/connector/xml_rpc.py (1)
  • send (66-78)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • send (66-69)
src/mccann_hub/odoolib/connection/_connection.py (3)
src/mccann_hub/odoolib/connector/_connector.py (1)
  • Connector (40-60)
src/mccann_hub/odoolib/model.py (1)
  • Model (38-160)
src/mccann_hub/odoolib/connection/_servicer.py (1)
  • Servicer (41-139)
src/mccann_hub/odoolib/connector/xml_rpc.py (3)
src/mccann_hub/odoolib/connector/_connector.py (1)
  • Connector (40-60)
src/mccann_hub/odoolib/connector/_sender.py (1)
  • send (38-42)
src/mccann_hub/odoolib/connector/json_rpc.py (1)
  • send (66-69)
tests/test_async.py (3)
tests/test.py (3)
  • setUp (45-46)
  • _conn (48-55)
  • _get_protocols (57-59)
src/mccann_hub/odoolib/main.py (1)
  • get_connection (64-88)
src/mccann_hub/odoolib/connection/_connection.py (2)
  • get_model (84-90)
  • async_get_user_context (76-82)
src/mccann_hub/odoolib/connection/_servicer.py (3)
src/mccann_hub/odoolib/connector/_connector.py (2)
  • Connector (40-60)
  • get_service (54-60)
src/mccann_hub/odoolib/service.py (1)
  • Service (38-78)
src/mccann_hub/odoolib/connection/authentication_error.py (1)
  • AuthenticationError (34-39)
🪛 YAMLlint (1.35.1)
test_containers/compose.yaml

[error] 24-24: trailing spaces

(trailing-spaces)


[warning] 31-31: too many blank lines

(1 > 0) (empty-lines)

🪛 Shellcheck (0.10.0)
test_containers/postgres/docker-ensure-initdb.sh

[warning] 30-30: Expanding an array without an index only gives the first element.

(SC2128)

test_containers/postgres/docker-entrypoint.sh

[warning] 163-163: psql appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 177-177: ShellCheck can't follow non-constant source. Use a directive to specify location.

(SC1090)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 201-201: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 209-209: This assignment is only seen by the forked process.

(SC2097)


[warning] 209-209: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 209-209: This expansion will not see the mentioned assignment.

(SC2098)


[warning] 214-214: This assignment is only seen by the forked process.

(SC2097)


[warning] 214-214: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 214-214: This expansion will not see the mentioned assignment.

(SC2098)


[warning] 274-275: Remove space after = if trying to assign a value (for empty string, use var='' ... ).

(SC1007)


[warning] 316-316: Expanding an array without an index only gives the first element.

(SC2128)

🪛 actionlint (1.7.4)
.github/workflows/test.yml

31-31: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

.github/workflows/publish.yml

67-67: the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 Ruff (0.8.2)
src/mccann_hub/odoolib/model.py

102-103: Use a single if statement instead of nested if statements

(SIM102)

🔇 Additional comments (58)
src/mccann_hub/odoolib/dates.py (7)

9-29: Well-formatted license header improvements.

These changes improve the license header formatting with consistent spacing and line structure, enhancing readability while maintaining the original license terms.


36-40: Good formatting of multi-line expression.

The multi-line formatting for DEFAULT_SERVER_DATETIME_FORMAT is well-structured and follows proper indentation practices, improving code readability.


42-53: Improved docstring formatting in str_to_datetime function.

The added blank line in the docstring improves readability by visually separating the function description from the implementation details.


54-64: Appropriate blank line spacing between functions.

The addition of a blank line between functions follows PEP 8 guidelines and improves visual separation of code blocks.


72-75: Well-structured multi-line return statement.

The reformatting of the return statement in str_to_time() spreads the expression across multiple lines with proper indentation, enhancing readability.


77-87: Improved docstring formatting in datetime_to_str function.

The added blank line in the docstring improves readability by visually separating the function description from the implementation notes.


88-98: Consistent spacing between functions.

The addition of blank lines between functions is applied consistently throughout the file, following good Python coding practices.

.gitignore (1)

1-175: Comprehensive .gitignore file looks good

This is a well-structured .gitignore file that covers all common Python development artifacts, package files, environments, and tool-specific directories. It follows best practices for Python projects.

README.rst (3)

16-16: Package name change looks good

The package name change from odoo-client-lib to mccann_hub-odoo_client_lib is correctly reflected in the installation instructions.


20-20: Import statement properly updated

The import statement has been correctly updated to reflect the new package structure.


65-70: Testing section is a helpful addition

The new testing section with Docker Compose instructions provides clear guidance for setting up a local test environment. This enhances the usability of the library.

src/mccann_hub/odoolib/connector/__init__.py (1)

6-12: Well-defined exports with all

The __all__ list properly defines which symbols should be exported from this module, which is a good practice for clarity and maintainability.

src/mccann_hub/odoolib/connection/authentication_error.py (1)

34-39: Well-designed error class

The AuthenticationError class is well-designed with a clear docstring explaining its purpose. It follows good practices for custom exceptions.

requirements.txt (1)

2-2:

Details

✅ Verification successful

Verify future certificate expiration date

The certifi package has a version date of 2025.1.31, which is in the future. This may cause compatibility issues if attempting to install in environments that validate package versions.


🏁 Script executed:

#!/bin/bash
# Check if this certifi version exists on PyPI
curl -s https://pypi.org/pypi/certifi/json | jq '.releases | keys' | grep "2025.1.31"

# Check the latest available version
curl -s https://pypi.org/pypi/certifi/json | jq '.info.version'

Length of output: 175


No action needed: certifi 2025.1.31 is a valid PyPI release

The pinned version (certifi==2025.1.31) is confirmed as the latest release on PyPI, so there are no compatibility issues related to its date-based versioning.

src/mccann_hub/odoolib/connector/json_rpcs.py (1)

41-51: Implementation looks good

The PROTOCOL constant is correctly defined as "jsonrpcs" and the implementation properly modifies the URL to use HTTPS instead of HTTP.

src/mccann_hub/odoolib/connection/__init__.py (1)

1-4: Clean implementation with properly defined public API

The module provides a clean interface by explicitly defining which symbols are exported through the __all__ list, following Python best practices.

src/mccann_hub/odoolib/connector/xml_rpcs.py (2)

39-42: Implementation looks good

The docstring correctly specifies that this connector uses the secured XMLRPC protocol, which appropriately differentiates it from the regular XmlRpcConnector.


51-58:

Details

✅ Verification successful

Verify SSL certificate validation

When implementing secure connections, it's important to ensure proper certificate validation. The current implementation inherits behavior from XmlRpcConnector, passing the transport parameter to the parent, but it's worth verifying that the default behavior enforces certificate validation.


🏁 Script executed:

#!/bin/bash
# Description: Search for certificate validation handling in XmlRpcConnector
# or related classes to ensure secure connections validate certificates

# Look for transport initialization code
rg -A 10 "transport.*=|SafeTransport" --type python

# Check for any certificate validation or context settings
rg -A 5 -B 5 "ssl.*context|verify.*cert|cert.*verify" --type python

Length of output: 185


🏁 Script executed:

#!/bin/bash
# Show the beginning of xml_rpcs.py to inspect imports and class definitions
sed -n '1,200p' src/mccann_hub/odoolib/connector/xml_rpcs.py

# Locate the parent class XmlRpcConnector definition within the connector directory
rg -n "class XmlRpcConnector" -R src/mccann_hub/odoolib/connector

# Find where SafeTransport is imported or defined in the connector module
rg -n "SafeTransport" -R src/mccann_hub/odoolib/connector

Length of output: 3386


🏁 Script executed:

#!/bin/bash
# Locate and inspect xml_rpc.py to verify how XmlRpcConnector handles SSL transports

# Show the path to xml_rpc.py
fd xml_rpc.py

# Print the first 200 lines of xml_rpc.py (class definitions and imports)
sed -n '1,200p' src/mccann_hub/odoolib/connector/xml_rpc.py

# Print lines 201–400 to see __init__ and make_connection implementations
sed -n '201,400p' src/mccann_hub/odoolib/connector/xml_rpc.py

Length of output: 3489


🏁 Script executed:

#!/bin/bash
# Inspect xmlrpc.client.SafeTransport implementation to confirm default SSL context behavior
python - << 'EOF'
import inspect, xmlrpc.client
print("=== SafeTransport Source ===")
print(inspect.getsource(xmlrpc.client.SafeTransport))
EOF

Length of output: 1119


🏁 Script executed:

#!/bin/bash
# Inspect http.client.HTTPSConnection to see how it handles SSL contexts and certificate validation
python - << 'EOF'
import inspect, http.client
print("=== HTTPSConnection Source ===")
print(inspect.getsource(http.client.HTTPSConnection))
EOF

Length of output: 2860


SSL Certificate Validation Confirmed

The default SafeTransport (with no custom context) uses http.client.HTTPSConnection, which creates a default SSL context via ssl._create_default_https_context()—this enforces CERT_REQUIRED and hostname checking. No changes are needed to ensure proper certificate validation.

tests/test_xml_rpc.py (2)

52-66: Well-implemented test using mocking

The test effectively mocks the ServerProxy to verify the send method's behavior, checking both the correct URL construction and method invocation.


47-49:

Details

❌ Incorrect review comment

Verify the logger name matches the module structure

The test expects the logger name to be "mccann_hub.odoolib.connector.xmlrpc", but based on the import path, it might be "mccann_hub.odoolib.connector.xml_rpc" instead. Verify this matches the actual implementation.


🏁 Script executed:

#!/bin/bash
# Check the actual logger name in the XmlRpcConnector class
rg -A 2 "_logger =" src/mccann_hub/odoolib/connector/xml_rpc.py

Length of output: 65


🏁 Script executed:

#!/bin/bash
# List all Python files in the connector directory
echo "Connector files:"  
fd src/mccann_hub/odoolib/connector -t f -e py

# Show any logger initialization in xmlrpc.py
echo -e "\nSearching for getLogger calls in xmlrpc.py:"  
rg -n "getLogger" src/mccann_hub/odoolib/connector/xmlrpc.py

# Show any _logger assignments in xmlrpc.py
echo -e "\nSearching for '_logger' in xmlrpc.py:"  
rg -n "_logger" src/mccann_hub/odoolib/connector/xmlrpc.py

Length of output: 1011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate xml_rpc.py in the repository
xml_file=$(find . -type f -name "xml_rpc.py" | head -n1)
echo "Inspecting: $xml_file"

# Show any logger initialization
echo -e "\n[getLogger calls]"
rg -n "getLogger" "$xml_file" || echo "No getLogger calls found in $xml_file"

# Show any _logger assignments
echo -e "\n[_logger assignments]"
rg -n "_logger" "$xml_file" || echo "No _logger assignments found in $xml_file"

Length of output: 806


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List all Python files in the connector directory
echo "Python files in connector:"
find src/mccann_hub/odoolib/connector -type f -name "*.py"

# Search for a property or method defining _logger
echo -e "\nSearching for '_logger' definitions:"
rg -n "def _logger" -B1 -A3 src/mccann_hub/odoolib/connector

# Search for direct assignments to _logger
echo -e "\nSearching for '_logger =' across connector:"
rg -n "_logger\s*=" -R src/mccann_hub/odoolib/connector

# Search for getLogger usage
echo -e "\nSearching for 'logging.getLogger' in connector:"
rg -n "logging\.getLogger" -R src/mccann_hub/odoolib/connector

# Search for import logging statements
echo -e "\nSearching for 'import logging':"
rg -n "import logging" -R src/mccann_hub/odoolib/connector

Length of output: 617


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect logger setup in the base connector class
file="src/mccann_hub/odoolib/connector/_connector.py"
echo "Inspecting: $file"

echo -e "\n[getLogger calls]"
rg -n "getLogger" "$file" || echo "No getLogger calls found"

echo -e "\n[__name__ references]"
rg -n "__name__" "$file" || echo "No __name__ references found"

echo -e "\n[Entire file head for context]"
head -n 200 "$file"

Length of output: 3172


Ignore the logger name suggestion
The Connector base class builds its logger name by joining __name__ (minus the module’s last segment) with PROTOCOL. In xml_rpc.py, PROTOCOL = "xmlrpc", so the resulting logger name is "mccann_hub.odoolib.connector.xmlrpc", exactly as asserted in the test. No changes are necessary.

Likely an incorrect or invalid review comment.

src/mccann_hub/odoolib/connector/_sender.py (1)

44-51: 🛠️ Refactor suggestion

Add error handling for asyncio loop detection

The async_send method uses asyncio.get_running_loop() which will raise a RuntimeError if no event loop is running. Consider adding error handling to create a new loop if none exists.

    async def async_send(self, service_name: str, method: str, *args) -> Any:
        """
        Async wrapper around `send` to allow non-blocking execution.
        """
-        loop = asyncio.get_running_loop()
+        try:
+            loop = asyncio.get_running_loop()
+        except RuntimeError:
+            loop = asyncio.new_event_loop()
+            asyncio.set_event_loop(loop)
        return await loop.run_in_executor(
            None, lambda: self.send(service_name, method, *args)
        )

Likely an incorrect or invalid review comment.

tests/test_json_rpc.py (2)

48-50: LGTM - Logger name assertion correctly matches expected pattern.

The test correctly verifies that the logger name follows the expected pattern, which is derived from the package path and the connector's PROTOCOL attribute.


52-78: Good test coverage for the JSON-RPC request/response flow.

The test thoroughly validates the JSON-RPC request structure, including verifying the correct fields in the request payload, proper randomization of the ID field, and correct extraction of results from the response.

.github/workflows/test-publish.yml (1)

62-66: Good approach using multiple index URLs.

Using both TestPyPI and PyPI as sources ensures dependencies that might not be available on TestPyPI can still be resolved from the main PyPI repository.

src/mccann_hub/odoolib/connector/_connector.py (1)

54-60: Good implementation of the service factory method.

The get_service method provides a clean interface for obtaining a Service instance bound to this connector, promoting good separation of concerns.

tests/test_service.py (1)

1-71: The Service test implementation looks complete and well-structured

The test coverage for the Service class is thorough, testing initialization, synchronous method calls, method caching, and method reflection capabilities.

tests/test_json_rpcs.py (1)

1-92: The JsonRpcsConnector test implementation is comprehensive

The test suite properly verifies initialization, successful requests, and error handling. Good job ensuring the JSON-RPC request structure is validated, including checking the proper format of parameters and handling of error responses.

tests/test.py (5)

40-40: LGTM: Updated import statement aligns with new package structure.

The import statement has been properly updated to use the new namespace structure.


48-55: LGTM: Improved method formatting.

The _conn method has been nicely reformatted with consistent indentation and parameter alignment, enhancing readability.


58-58: LGTM: Added helpful docstring.

Adding a descriptive docstring to the _get_protocols method improves code documentation.


73-76: LGTM: Enhanced test assertions.

The test_user_context method has been improved with more specific assertions that validate the presence of expected keys in the context dictionary.


89-89: LGTM: Consistent string quotes.

Updated to use double quotes in the main guard, which is more consistent with Python style conventions.

src/mccann_hub/odoolib/service.py (2)

38-51: LGTM: Well-structured Service class initialization.

The Service class is well-designed with proper constructor parameters, docstrings, and type hints. The logger initialization with a service-specific name is a good practice for debugging.


52-78: LGTM: Elegant implementation of dynamic method proxying.

The __getattr__ method provides an elegant solution for dynamically creating proxy functions. The implementation:

  1. Supports both synchronous and asynchronous calls
  2. Includes detailed logging for debugging
  3. Attaches the async proxy to the sync proxy for a clean API

This design makes the service easy to use while maintaining flexibility.

test_containers/postgres/Dockerfile (3)

1-5: Warning: This is a generated file that should not be edited directly.

The comments at the top indicate this Dockerfile is generated via "apply-templates.sh" and should not be edited directly. Any manual changes might be overwritten when the template is reapplied.

Consider documenting the template source and generation process in your project documentation to ensure future maintenance follows the correct workflow.


189-190: LGTM: Intentional VOLUME directive commenting.

Good decision to comment out the VOLUME directive to enable database snapshots. This allows for more flexible container usage in testing environments.


196-219: LGTM: Well-documented signal handling.

Excellent documentation explaining the choice of SIGINT as the STOPSIGNAL and providing references to PostgreSQL documentation. This helps future maintainers understand the rationale behind this configuration.

tests/test_connection.py (4)

37-46: LGTM: Well-structured test setup.

The test class follows good practices with a proper setUp method that creates a mock connector and initializes the Connection with test parameters.


47-52: LGTM: Thorough initialization testing.

The test_initialization method properly verifies that the Connection constructor correctly sets the instance properties.


53-63: LGTM: Effective context retrieval testing.

The test_get_user_context method:

  1. Properly mocks the model return value
  2. Verifies the context is retrieved correctly
  3. Confirms the context is cached in the connection
  4. Validates that the correct method was called with the right parameters

This comprehensive testing ensures the get_user_context method works as expected.


64-68: LGTM: Good model instantiation testing.

The test_get_model method validates that the get_model method returns the correct Model instance with the proper model name and connection reference.

test_containers/postgres/docker-ensure-initdb.sh (3)

1-15: Well-structured script header with clear documentation.

The script header clearly explains the three main use cases for this script, which is excellent for maintainability and future developers.


33-53: Sound initialization logic with appropriate error checking.

The script properly checks for an empty database directory before initializing, verifies minimum environment variables, checks directory permissions, and sets up PostgreSQL authentication correctly. The script also exports PGPASSWORD when needed for authentication.


54-71: Smart script self-identification and error handling.

The script intelligently determines its own filename and changes behavior based on whether it's called as docker-ensure-initdb.sh (no-op if database exists) or docker-enforce-initdb.sh (error if database exists), which aligns well with the stated use cases in the header.

tests/test_model.py (3)

35-43: Well-structured test setup with appropriate mocking.

The test class properly sets up a mock connection with all necessary attributes required by the Model class. Good use of MagicMock for simulating the execute_kw method.


49-67: Good coverage of proxy method call scenarios.

The tests comprehensively cover different proxy method call scenarios, including single ID, multiple IDs, and the case where an ID is not found in the result set.


69-78: Thorough testing of search_read functionality.

The tests properly verify the search_read functionality, including the case where no results are found. Using MagicMock for the search and read methods is a good approach.

tests/test_async.py (4)

34-50: Well-structured async test class setup.

The test class properly uses unittest.IsolatedAsyncioTestCase for async tests and provides helper methods for connection creation and protocol selection, which promotes code reuse across tests.


51-59: Good basic async read test.

The test properly verifies async read functionality across different protocols and includes appropriate assertions to validate the result.


60-68: Improved user context test with proper assertions.

The test for user context retrieval now includes assertions to verify both the structure of the context (it's a dictionary) and its expected content (contains 'lang' and 'tz' keys).


70-83: Comprehensive async search and count test.

The test verifies that the async search and count methods work correctly by comparing the count result with the length of search results, providing good validation of these core async methods.

src/mccann_hub/odoolib/connector/xml_rpc.py (1)

39-65: Well-documented connector class with clear initialization.

The XmlRpcConnector class is properly documented with clear parameter descriptions. The URL construction logic correctly handles both with and without version paths.

src/mccann_hub/odoolib/connection/_servicer.py (2)

41-77: Code looks good - well-organized class structure for authentication.

The Servicer class is well-designed with clear separation of concerns. It properly handles authentication parameters and provides flexible login configuration.


131-139: Effective service proxy implementation.

The get_service method properly delegates to the connector for creating service proxies while maintaining clean abstraction.

setup.py (2)

42-44: LGTM - Good package organization using namespace packages.

The use of find_namespace_packages with the where="src" parameter is a good modern practice for Python packaging.


52-61: Python version classifiers aligned with requirements.

The classifiers correctly specify Python 3.9 through 3.13 compatibility, matching the python_requires=">=3.9" requirement.

test_containers/postgres/docker-entrypoint.sh (2)

1-25: Well-implemented file_env function for handling environment variables.

The file_env function properly handles reading environment variables from files, which is a good security practice for Docker containers, especially when working with secrets.


304-355: Main function is well-structured.

The _main function follows a logical flow for initializing and configuring PostgreSQL within a Docker container. It properly handles different scenarios and delegates to specialized functions.

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 316-316: Expanding an array without an index only gives the first element.

(SC2128)

retries: 5

db:
image: ghcr.io/jabez007/odoo-client-lib
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Specify a specific version tag for database image

Using an image without a specific version tag can lead to unexpected changes when the image is updated. Consider using a specific version tag for better reproducibility.

-    image: ghcr.io/jabez007/odoo-client-lib
+    image: ghcr.io/jabez007/odoo-client-lib:1.0.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
image: ghcr.io/jabez007/odoo-client-lib
image: ghcr.io/jabez007/odoo-client-lib:1.0.0

Comment on lines +33 to +43
class Sender(object):
"""
The base abstract class for sending RPC requests
"""

def send(self, service_name: str, method: str, *args) -> Any:
"""
stub out method for children to override
"""
return {"service_name": service_name, "method": method, "args": args}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve the base class implementation

The current send method returns a dictionary rather than raising NotImplementedError. Since this is meant to be an abstract method, it would be better to either:

  1. Make it a proper abstract method, or
  2. Raise NotImplementedError to prevent accidental usage of the base implementation
from typing import Any
+from abc import ABC, abstractmethod


-class Sender(object):
+class Sender(ABC):
    """
    The base abstract class for sending RPC requests
    """

+    @abstractmethod
    def send(self, service_name: str, method: str, *args) -> Any:
        """
-        stub out method for children to override
+        Send a request to the specified service and method with the given arguments.
+
+        :param service_name: The name of the service to call
+        :param method: The method name to call on the service
+        :param args: Additional arguments to pass to the method
+        :return: The result of the method call
        """
-        return {"service_name": service_name, "method": method, "args": args}
+        raise NotImplementedError("Subclasses must implement send()")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Sender(object):
"""
The base abstract class for sending RPC requests
"""
def send(self, service_name: str, method: str, *args) -> Any:
"""
stub out method for children to override
"""
return {"service_name": service_name, "method": method, "args": args}
from typing import Any
from abc import ABC, abstractmethod
class Sender(ABC):
"""
The base abstract class for sending RPC requests
"""
@abstractmethod
def send(self, service_name: str, method: str, *args) -> Any:
"""
Send a request to the specified service and method with the given arguments.
:param service_name: The name of the service to call
:param method: The method name to call on the service
:param args: Additional arguments to pass to the method
:return: The result of the method call
"""
raise NotImplementedError("Subclasses must implement send()")

# process initializer files, based on file extensions and permissions
docker_process_init_files() {
# psql here for backwards compatibility "${psql[@]}"
psql=( docker_process_sql )
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Unused variable warning.

The psql variable appears to be declared but not used directly (detected by Shellcheck). If it's used externally, consider adding a comment or exporting it.


🏁 Script executed:

#!/bin/bash
# Check if `psql` array is used elsewhere in the script
grep -n "psql\[" test_containers/postgres/docker-entrypoint.sh

Length of output: 122


Remove or properly expose the unused psql array

The assignment

psql=( docker_process_sql )

in test_containers/postgres/docker-entrypoint.sh (lines 162–163) is never actually invoked—the only occurrence of psql[ is in a comment. Since Bash arrays aren’t exported, this declaration is effectively dead code and triggers a Shellcheck warning. You should either:

• Remove the psql assignment (and related comment) if it’s no longer needed.
• Or, if you really need it for backwards-compatibility, explicitly invoke ${psql[@]} where required or export the function instead (e.g. export -f docker_process_sql).

[test_containers/postgres/docker-entrypoint.sh:162–163]

🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 163-163: psql appears unused. Verify use (or export if used externally).

(SC2034)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
.github/workflows/test.yml (1)

29-33: Upgrade actions/setup-python to v5
The action version @v4 is outdated and may not support the latest runners. Please bump to @v5:

-      - name: Setup Python
-        uses: actions/setup-python@v4
+      - name: Setup Python
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python_version }}
🧰 Tools
🪛 actionlint (1.7.4)

30-30: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🧹 Nitpick comments (3)
.github/workflows/test.yml (3)

3-8: Expand workflow triggers to include pull requests
Currently the workflow only runs on direct pushes (excluding main/master). It’s recommended to also trigger on pull_request events so CI runs on external contributor PRs:

 on:
-  push:
-    branches-ignore:
-      - main
-      - master
+  push:
+    branches-ignore:
+      - main
+      - master
+  pull_request:
+    branches-ignore:
+      - main
+      - master

25-28: Consider using GitHub Actions services for containers
Instead of manually invoking docker compose, you can leverage the services key under the job to spin up PostgreSQL and Odoo. This simplifies networking and readiness handling:

jobs:
  run_tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:13
        ports:
          - 5432:5432
      odoo:
        image: odoo:15
        ports:
          - 8069:8069
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      # …omit manual docker compose step…

34-37: Cache pip dependencies to speed up CI
Use actions/cache to reuse the pip cache across runs, reducing install time:

-      - name: Install requirements
-        run: |
-          pip install -r requirements.txt
+      - name: Cache pip
+        uses: actions/cache@v3
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
+      - name: Install requirements
+        run: pip install -r requirements.txt
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72b8fb8 and cb24102.

📒 Files selected for processing (6)
  • .github/workflows/publish.yml (1 hunks)
  • .github/workflows/test.yml (1 hunks)
  • pyproject.toml (1 hunks)
  • setup.py (2 hunks)
  • src/mccann_hub/odoolib/connector/json_rpc.py (1 hunks)
  • src/mccann_hub/odoolib/main.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/mccann_hub/odoolib/main.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • .github/workflows/publish.yml
  • src/mccann_hub/odoolib/connector/json_rpc.py
  • pyproject.toml
  • setup.py
🧰 Additional context used
🪛 actionlint (1.7.4)
.github/workflows/test.yml

30-30: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🔇 Additional comments (1)
.github/workflows/test.yml (1)

38-41: Verify test discovery invocation
The PYTHONPATH=src python -m unittest discover -s tests step correctly locates and runs your unit tests. No changes needed here.

@jabez007 jabez007 merged commit 982344d into master Apr 24, 2025
8 checks passed
@jabez007 jabez007 deleted the refactor-with-updates branch April 24, 2025 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant