From 267ac50fb9a4d9500bb196c8d413349059a59686 Mon Sep 17 00:00:00 2001 From: "Aung Ko Ko Lin (Quartile)" <45355704+AungKoKoLin1997@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:10:00 +0700 Subject: [PATCH 1/9] [3875][ADD] base_api_connection (#132) [ADD] base_api_connection --- base_api_connection/README.rst | 58 +++ base_api_connection/__init__.py | 1 + base_api_connection/__manifest__.py | 16 + base_api_connection/models/__init__.py | 2 + base_api_connection/models/api_call_mixin.py | 56 +++ base_api_connection/models/api_config.py | 35 ++ base_api_connection/readme/CONFIGURE.rst | 2 + base_api_connection/readme/DESCRIPTION.rst | 1 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 415 ++++++++++++++++++ .../views/api_config_views.xml | 49 +++ 11 files changed, 637 insertions(+) create mode 100644 base_api_connection/README.rst create mode 100644 base_api_connection/__init__.py create mode 100644 base_api_connection/__manifest__.py create mode 100644 base_api_connection/models/__init__.py create mode 100644 base_api_connection/models/api_call_mixin.py create mode 100644 base_api_connection/models/api_config.py create mode 100644 base_api_connection/readme/CONFIGURE.rst create mode 100644 base_api_connection/readme/DESCRIPTION.rst create mode 100644 base_api_connection/security/ir.model.access.csv create mode 100644 base_api_connection/static/description/index.html create mode 100644 base_api_connection/views/api_config_views.xml diff --git a/base_api_connection/README.rst b/base_api_connection/README.rst new file mode 100644 index 0000000..abe4f9d --- /dev/null +++ b/base_api_connection/README.rst @@ -0,0 +1,58 @@ +=================== +Base API Connection +=================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-qrtl%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/qrtl/axls-oca/tree/16.0/base_api_connection + :alt: qrtl/axls-oca + +|badge1| |badge2| |badge3| + +This module facilitates API connections between Odoo and other web services. It is not usable on its own; rather, it is a low-level module intended to serve as a base for others. An example of such a module is 'project_task_capture'. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Go to Settings > API Connection > API Configuration. +2. Create an API configuration record with 'base_url', 'header_api_key_string', 'code', and 'x_api_key'. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Quartile Limited + +Maintainers +~~~~~~~~~~~ + +This module is part of the `qrtl/axls-oca `_ project on GitHub. + +You are welcome to contribute. diff --git a/base_api_connection/__init__.py b/base_api_connection/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/base_api_connection/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_api_connection/__manifest__.py b/base_api_connection/__manifest__.py new file mode 100644 index 0000000..e58ae28 --- /dev/null +++ b/base_api_connection/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +{ + "name": "Base API Connection", + "version": "16.0.1.0.0", + "category": "API", + "website": "https://www.quartile.co", + "author": "Quartile Limited, Odoo Community Association (OCA)", + "license": "LGPL-3", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/api_config_views.xml", + ], + "installable": True, +} diff --git a/base_api_connection/models/__init__.py b/base_api_connection/models/__init__.py new file mode 100644 index 0000000..fbd76e5 --- /dev/null +++ b/base_api_connection/models/__init__.py @@ -0,0 +1,2 @@ +from . import api_config +from . import api_call_mixin diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py new file mode 100644 index 0000000..fa13caf --- /dev/null +++ b/base_api_connection/models/api_call_mixin.py @@ -0,0 +1,56 @@ +# Copyright 2023 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +import logging + +import requests + +from odoo import _, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class APICallMixin(models.AbstractModel): + _name = "api.call.mixin" + _description = "API Call Mixin" + + def get_api_key(self, config): + return config.api_key + + def make_api_call( + self, + code, + external_system="generic", + endpoint=None, + custom_headers=None, + params=None, + json=None, + http_method="get", + ): + config = self.env["api.config"].search( + [("external_system", "=", external_system), ("code", "=", code)], limit=1 + ) + if not config: + raise UserError(_("API configuration not found.")) + url = f"{config.base_url}/{endpoint}" + headers = {"Content-Type": "application/json"} + if custom_headers: + headers.update(custom_headers) + api_key = self.get_api_key(config) + headers[config.header_api_key_string] = api_key + function = getattr(requests, http_method) + kwargs = {"headers": headers, "params": params} + if json: + kwargs["json"] = json + try: + response = function(url, **kwargs) + response.raise_for_status() # Raises HTTPError for bad responses + _logger.info( + f"Successful API call to {url}. Response status code: {response.status_code}" + ) + except requests.exceptions.HTTPError as e: + raise UserError(f"HTTP Error: {str(e)}") from e + except requests.exceptions.RequestException as e: + raise UserError(f"Request Error: {str(e)}") from e + return response diff --git a/base_api_connection/models/api_config.py b/base_api_connection/models/api_config.py new file mode 100644 index 0000000..d4c7601 --- /dev/null +++ b/base_api_connection/models/api_config.py @@ -0,0 +1,35 @@ +# Copyright 2023 Quartile Limited +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ApiConfig(models.Model): + _name = "api.config" + _description = "API Configuration" + + name = fields.Char(required=True) + code = fields.Char( + required=True, + help="Expected to act as an identifier of the API configuration " + "record along with External System.", + ) + base_url = fields.Char(string="URL") + header_api_key_string = fields.Char( + required=True, + help="This string will be used as the key in the API header for the API key value.", + ) + external_system = fields.Selection( + [("generic", "Generic")], default="generic", required=True + ) + api_key = fields.Char(string="Api Key or Token", required=True) + + @api.constrains("code") + def _check_code(self): + for record in self: + existing_rec = self.search( + [("code", "=", record.code), ("id", "!=", record.id)], + ) + if existing_rec: + raise ValidationError(_("Code must be unique.")) diff --git a/base_api_connection/readme/CONFIGURE.rst b/base_api_connection/readme/CONFIGURE.rst new file mode 100644 index 0000000..930d150 --- /dev/null +++ b/base_api_connection/readme/CONFIGURE.rst @@ -0,0 +1,2 @@ +1. Go to Settings > API Connection > API Configuration. +2. Create an API configuration record with 'base_url', 'header_api_key_string', 'code', and 'x_api_key'. diff --git a/base_api_connection/readme/DESCRIPTION.rst b/base_api_connection/readme/DESCRIPTION.rst new file mode 100644 index 0000000..7eb3c25 --- /dev/null +++ b/base_api_connection/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module facilitates API connections between Odoo and other web services. It is not usable on its own; rather, it is a low-level module intended to serve as a base for others. An example of such a module is 'project_task_capture'. diff --git a/base_api_connection/security/ir.model.access.csv b/base_api_connection/security/ir.model.access.csv new file mode 100644 index 0000000..cdaee61 --- /dev/null +++ b/base_api_connection/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_api_config_manager,api.config,model_api_config,base.group_system,1,1,1,1 diff --git a/base_api_connection/static/description/index.html b/base_api_connection/static/description/index.html new file mode 100644 index 0000000..36c315c --- /dev/null +++ b/base_api_connection/static/description/index.html @@ -0,0 +1,415 @@ + + + + + + +Base API Connection + + + +
+

Base API Connection

+ + +

Beta License: LGPL-3 qrtl/axls-oca

+

This module facilitates API connections between Odoo and other web services. It is not usable on its own; rather, it is a low-level module intended to serve as a base for others. An example of such a module is ‘project_task_capture’.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Go to Settings > API Connection > API Configuration.
  2. +
  3. Create an API configuration record with ‘base_url’, ‘header_api_key_string’, ‘code’, and ‘x_api_key’.
  4. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile Limited
  • +
+
+
+

Maintainers

+

This module is part of the qrtl/axls-oca project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/base_api_connection/views/api_config_views.xml b/base_api_connection/views/api_config_views.xml new file mode 100644 index 0000000..fb9ab2b --- /dev/null +++ b/base_api_connection/views/api_config_views.xml @@ -0,0 +1,49 @@ + + + + api.config.tree + api.config + + + + + + + + api.config.form + api.config + +
+ + + + + + + + + + +
+
+
+ + API Configuration + api.config + tree,form + {} + [] + + + +
From ec08e2e85552f1fc95d73e250edf1a5d1eec4cb0 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Thu, 17 Jul 2025 03:27:24 +0000 Subject: [PATCH 2/9] [IMP] base_api_connection: pre-commit stuff --- setup/base_api_connection/odoo/addons/base_api_connection | 1 + setup/base_api_connection/setup.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 120000 setup/base_api_connection/odoo/addons/base_api_connection create mode 100644 setup/base_api_connection/setup.py diff --git a/setup/base_api_connection/odoo/addons/base_api_connection b/setup/base_api_connection/odoo/addons/base_api_connection new file mode 120000 index 0000000..ff04bc6 --- /dev/null +++ b/setup/base_api_connection/odoo/addons/base_api_connection @@ -0,0 +1 @@ +../../../../base_api_connection \ No newline at end of file diff --git a/setup/base_api_connection/setup.py b/setup/base_api_connection/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/base_api_connection/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 9f9b1e00b2f4a2ce0b41f684affbde535e404e2b Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Thu, 17 Jul 2025 04:06:43 +0000 Subject: [PATCH 3/9] [ADD] base_api_connection --- base_api_connection/__manifest__.py | 6 +++--- base_api_connection/models/api_call_mixin.py | 2 +- base_api_connection/models/api_config.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/base_api_connection/__manifest__.py b/base_api_connection/__manifest__.py index e58ae28..68315c2 100644 --- a/base_api_connection/__manifest__.py +++ b/base_api_connection/__manifest__.py @@ -1,11 +1,11 @@ -# Copyright 2023 Quartile Limited +# Copyright 2023 Quartile (https://www.quartile.co) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). { "name": "Base API Connection", - "version": "16.0.1.0.0", + "version": "15.0.1.0.0", "category": "API", "website": "https://www.quartile.co", - "author": "Quartile Limited, Odoo Community Association (OCA)", + "author": "Quartile, Odoo Community Association (OCA)", "license": "LGPL-3", "depends": ["base"], "data": [ diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py index fa13caf..a4823da 100644 --- a/base_api_connection/models/api_call_mixin.py +++ b/base_api_connection/models/api_call_mixin.py @@ -1,4 +1,4 @@ -# Copyright 2023 Quartile Limited +# Copyright 2023 Quartile (https://www.quartile.co) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). import logging diff --git a/base_api_connection/models/api_config.py b/base_api_connection/models/api_config.py index d4c7601..40a750d 100644 --- a/base_api_connection/models/api_config.py +++ b/base_api_connection/models/api_config.py @@ -1,4 +1,4 @@ -# Copyright 2023 Quartile Limited +# Copyright 2023 Quartile (https://www.quartile.co) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). from odoo import _, api, fields, models From ba750d859db45d5045c7650f1366d2c8cd1cbf91 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 25 Jul 2025 08:24:02 +0000 Subject: [PATCH 4/9] [IMP] base_api_connection: add token_type --- base_api_connection/models/api_call_mixin.py | 2 ++ base_api_connection/models/api_config.py | 1 + base_api_connection/views/api_config_views.xml | 1 + 3 files changed, 4 insertions(+) diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py index a4823da..5b966c0 100644 --- a/base_api_connection/models/api_call_mixin.py +++ b/base_api_connection/models/api_call_mixin.py @@ -16,6 +16,8 @@ class APICallMixin(models.AbstractModel): _description = "API Call Mixin" def get_api_key(self, config): + if config.token_type: + return f"{config.token_type} {config.api_key}" return config.api_key def make_api_call( diff --git a/base_api_connection/models/api_config.py b/base_api_connection/models/api_config.py index 40a750d..e3594fc 100644 --- a/base_api_connection/models/api_config.py +++ b/base_api_connection/models/api_config.py @@ -23,6 +23,7 @@ class ApiConfig(models.Model): external_system = fields.Selection( [("generic", "Generic")], default="generic", required=True ) + token_type = fields.Char("Api Token Type") api_key = fields.Char(string="Api Key or Token", required=True) @api.constrains("code") diff --git a/base_api_connection/views/api_config_views.xml b/base_api_connection/views/api_config_views.xml index fb9ab2b..2817d82 100644 --- a/base_api_connection/views/api_config_views.xml +++ b/base_api_connection/views/api_config_views.xml @@ -21,6 +21,7 @@ + From 9cd4d6ddbc4ecd6df9b4057dbed9114fb6eddb5f Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 25 Jul 2025 08:32:07 +0000 Subject: [PATCH 5/9] fix pre-commit --- base_api_connection/models/api_config.py | 2 +- base_api_connection/views/api_config_views.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/base_api_connection/models/api_config.py b/base_api_connection/models/api_config.py index e3594fc..9f9b332 100644 --- a/base_api_connection/models/api_config.py +++ b/base_api_connection/models/api_config.py @@ -23,7 +23,7 @@ class ApiConfig(models.Model): external_system = fields.Selection( [("generic", "Generic")], default="generic", required=True ) - token_type = fields.Char("Api Token Type") + token_type = fields.Char("Api Token Type") api_key = fields.Char(string="Api Key or Token", required=True) @api.constrains("code") diff --git a/base_api_connection/views/api_config_views.xml b/base_api_connection/views/api_config_views.xml index 2817d82..05ffedd 100644 --- a/base_api_connection/views/api_config_views.xml +++ b/base_api_connection/views/api_config_views.xml @@ -21,7 +21,7 @@ - + From ca21b0286be0b412d8cdbc1e49deb8b662ff3092 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Tue, 26 Aug 2025 06:18:55 +0000 Subject: [PATCH 6/9] upd for http error detail --- base_api_connection/models/api_call_mixin.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py index 5b966c0..45e444b 100644 --- a/base_api_connection/models/api_call_mixin.py +++ b/base_api_connection/models/api_call_mixin.py @@ -52,7 +52,20 @@ def make_api_call( f"Successful API call to {url}. Response status code: {response.status_code}" ) except requests.exceptions.HTTPError as e: - raise UserError(f"HTTP Error: {str(e)}") from e + r = getattr(e, "response", None) + status = getattr(r, "status_code", "n/a") + rid = getattr(getattr(r, "headers", {}), "get", lambda *_: None)( + "x-request-id" + ) + body_text = "" + if r is not None: + try: + body_text = json.dumps(r.json(), ensure_ascii=False) + except Exception: + body_text = r.text or "" + raise UserError( + f"HTTP Error {status} (x-request-id={rid})\n{body_text[:2000]}" + ) from e except requests.exceptions.RequestException as e: raise UserError(f"Request Error: {str(e)}") from e return response From d1c193153e099253aad5ff37d539618b9fca00e7 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Tue, 26 Aug 2025 08:09:02 +0000 Subject: [PATCH 7/9] add logger about http error --- base_api_connection/models/api_call_mixin.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py index 45e444b..5de6c0e 100644 --- a/base_api_connection/models/api_call_mixin.py +++ b/base_api_connection/models/api_call_mixin.py @@ -52,20 +52,13 @@ def make_api_call( f"Successful API call to {url}. Response status code: {response.status_code}" ) except requests.exceptions.HTTPError as e: - r = getattr(e, "response", None) - status = getattr(r, "status_code", "n/a") - rid = getattr(getattr(r, "headers", {}), "get", lambda *_: None)( - "x-request-id" + _logger.error( + "HTTP Error: %s \n%s", + e.response.status_code, + e.response.text[:500], + exc_info=True, ) - body_text = "" - if r is not None: - try: - body_text = json.dumps(r.json(), ensure_ascii=False) - except Exception: - body_text = r.text or "" - raise UserError( - f"HTTP Error {status} (x-request-id={rid})\n{body_text[:2000]}" - ) from e + raise UserError(f"HTTP Error: {str(e)}") from e except requests.exceptions.RequestException as e: raise UserError(f"Request Error: {str(e)}") from e return response From 5084f36eb9d0ca9d3be0d7d4c4ab4a0e37ad3e8c Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Thu, 18 Sep 2025 02:56:02 +0000 Subject: [PATCH 8/9] add conditional branch about endpoint and params --- base_api_connection/models/api_call_mixin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py index 5de6c0e..9ef159f 100644 --- a/base_api_connection/models/api_call_mixin.py +++ b/base_api_connection/models/api_call_mixin.py @@ -35,14 +35,18 @@ def make_api_call( ) if not config: raise UserError(_("API configuration not found.")) - url = f"{config.base_url}/{endpoint}" + url = f"{config.base_url}" + if endpoint: + url = f"{url}/{endpoint}" headers = {"Content-Type": "application/json"} if custom_headers: headers.update(custom_headers) api_key = self.get_api_key(config) headers[config.header_api_key_string] = api_key function = getattr(requests, http_method) - kwargs = {"headers": headers, "params": params} + kwargs = {"headers": headers} + if params: + kwargs["params"] = params if json: kwargs["json"] = json try: From e9aa383f620a8e5374cf1ed38192482ba533972b Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Wed, 24 Sep 2025 06:00:16 +0000 Subject: [PATCH 9/9] upd from Yoshi-san's comment 1 --- base_api_connection/models/api_call_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base_api_connection/models/api_call_mixin.py b/base_api_connection/models/api_call_mixin.py index 9ef159f..2974b9b 100644 --- a/base_api_connection/models/api_call_mixin.py +++ b/base_api_connection/models/api_call_mixin.py @@ -35,7 +35,7 @@ def make_api_call( ) if not config: raise UserError(_("API configuration not found.")) - url = f"{config.base_url}" + url = f"{config.base_url.rstrip('/')}" if endpoint: url = f"{url}/{endpoint}" headers = {"Content-Type": "application/json"} @@ -47,7 +47,7 @@ def make_api_call( kwargs = {"headers": headers} if params: kwargs["params"] = params - if json: + if json is not None: kwargs["json"] = json try: response = function(url, **kwargs)