From 01e9d96fd32430ea8a7e688690e87b7a5e324819 Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Mon, 30 Oct 2023 17:31:10 +0100 Subject: [PATCH 1/2] [ADD] base_external_system_http: new module --- base_external_system_http/README.rst | 9 ++ base_external_system_http/__init__.py | 2 + base_external_system_http/__manifest__.py | 20 ++++ .../demo/external_system_demo.xml | 17 +++ .../demo/external_system_endpoint_demo.xml | 14 +++ base_external_system_http/models/__init__.py | 4 + .../models/external_system.py | 17 +++ .../models/external_system_adapter_http.py | 101 ++++++++++++++++++ .../models/external_system_endpoint.py | 20 ++++ .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 1142 bytes base_external_system_http/tests/__init__.py | 3 + .../tests/test_external_system.py | 43 ++++++++ .../views/external_system.xml | 22 ++++ 14 files changed, 275 insertions(+) create mode 100644 base_external_system_http/README.rst create mode 100644 base_external_system_http/__init__.py create mode 100644 base_external_system_http/__manifest__.py create mode 100755 base_external_system_http/demo/external_system_demo.xml create mode 100644 base_external_system_http/demo/external_system_endpoint_demo.xml create mode 100644 base_external_system_http/models/__init__.py create mode 100644 base_external_system_http/models/external_system.py create mode 100644 base_external_system_http/models/external_system_adapter_http.py create mode 100644 base_external_system_http/models/external_system_endpoint.py create mode 100644 base_external_system_http/security/ir.model.access.csv create mode 100644 base_external_system_http/static/description/icon.png create mode 100644 base_external_system_http/tests/__init__.py create mode 100644 base_external_system_http/tests/test_external_system.py create mode 100644 base_external_system_http/views/external_system.xml diff --git a/base_external_system_http/README.rst b/base_external_system_http/README.rst new file mode 100644 index 000000000..ad3191994 --- /dev/null +++ b/base_external_system_http/README.rst @@ -0,0 +1,9 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: https://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================== +Hc external system +================== + +A connection to external systems for Human Company. diff --git a/base_external_system_http/__init__.py b/base_external_system_http/__init__.py new file mode 100644 index 000000000..c32fd62b7 --- /dev/null +++ b/base_external_system_http/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import models diff --git a/base_external_system_http/__manifest__.py b/base_external_system_http/__manifest__.py new file mode 100644 index 000000000..09b87a745 --- /dev/null +++ b/base_external_system_http/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2023 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "External System HTTP", + "version": "10.0.1.0.0", + "author": "Therp BV, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Base", + "website": "https://github.com/OCA/server-tools", + "summary": "HTTP Connector for External Systems", + "depends": ["base_external_system"], + "data": [ + "demo/external_system_demo.xml", + "demo/external_system_endpoint_demo.xml", + "security/ir.model.access.csv", + "views/external_system.xml", + ], + "installable": True, + "application": False, +} diff --git a/base_external_system_http/demo/external_system_demo.xml b/base_external_system_http/demo/external_system_demo.xml new file mode 100755 index 000000000..e851931a3 --- /dev/null +++ b/base_external_system_http/demo/external_system_demo.xml @@ -0,0 +1,17 @@ + + + + + + Example Connection to GitHub for Testing + external.system.adapter.http + https + github.com + /OCA + + + + diff --git a/base_external_system_http/demo/external_system_endpoint_demo.xml b/base_external_system_http/demo/external_system_endpoint_demo.xml new file mode 100644 index 000000000..e6e5a664c --- /dev/null +++ b/base_external_system_http/demo/external_system_endpoint_demo.xml @@ -0,0 +1,14 @@ + + + + + + + tools + /server-tools + + + diff --git a/base_external_system_http/models/__init__.py b/base_external_system_http/models/__init__.py new file mode 100644 index 000000000..f2f6d84ba --- /dev/null +++ b/base_external_system_http/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import external_system_endpoint # Must be before external_system! +from . import external_system +from . import external_system_adapter_http diff --git a/base_external_system_http/models/external_system.py b/base_external_system_http/models/external_system.py new file mode 100644 index 000000000..07fbed4b7 --- /dev/null +++ b/base_external_system_http/models/external_system.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ExternalSystem(models.Model): + """Extend base external system""" + + _inherit = "external.system" + + endpoint_ids = fields.One2many( + comodel_name="external.system.endpoint", + inverse_name="system_id", + help="Endpoints on remote system", + ) diff --git a/base_external_system_http/models/external_system_adapter_http.py b/base_external_system_http/models/external_system_adapter_http.py new file mode 100644 index 000000000..2da76419a --- /dev/null +++ b/base_external_system_http/models/external_system_adapter_http.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Extend base adapter model for connections through http(s).""" +import json +import logging + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +# You must initialize logging, otherwise you'll not see debug output. +logging.basicConfig() +logging.getLogger().setLevel(logging.DEBUG) +requests_log = logging.getLogger("requests.packages.urllib3") +requests_log.setLevel(logging.DEBUG) +requests_log.propagate = True + + +_logger = logging.getLogger(__name__) # pylint: disable=invalid-name + + +class ExternalSystemAdapterHTTP(models.AbstractModel): + """HTTP external system Adapter""" + + _inherit = "external.system.adapter" + _name = "external.system.adapter.http" + _description = __doc__ + + @api.model + def external_get_client(self): + """Return self as the client.""" + return self + + @api.model + def external_destroy_client(self, client): + """If needed logout of server.""" + pass + + def get(self, endpoint=None, params=None, **kwargs): + """Pass transparantly to request.get, but check response.""" + url = self._get_url(endpoint=endpoint) + _logger.debug("Will get data from %s", url) + response = requests.get(url, params=params, **kwargs) + if response.status_code != 200: + message = _("Data could not be retrieved from endpoint %s: %s") % ( + endpoint, + str(response.text), + ) + _logger.error(message) + raise UserError(message) + _logger.info( + _("Data succesfully retrieved from endpoint %s"), + endpoint, + ) + return response + + def post(self, endpoint=None, data=None, json=None, **kwargs): + """Post data to http server.""" + url = self._get_url(endpoint=endpoint) + _logger.debug("Will post data to %s", url) + response = requests.post(url, data=data, json=json, **kwargs) + if response.status_code not in (200, 201): + message = _("Data could not be pushed to endpoint %s: %s") % ( + endpoint, + str(response.text), + ) + _logger.error(message) + raise UserError(message) + + def _get_url(self, endpoint=None, url_suffix=None): + """Make full url for endpoint. + + The configured remote_path, endpoint and the passed url_suffix + must, if used, always start with "/". + """ + system = self.system_id + if endpoint: + endpoint_model = self.env["external.system.endpoint"] + endpoint_record = endpoint_model.search( + [ + ("system_id", "=", system.id), + ("name", "=", endpoint), + ], + limit=1 + ) + if not endpoint_record: + raise UserError( + _("Endpoint %s not found on system %s") + % (endpoint, system.name) + ) + url = "%(scheme)s://%(host)s%(port)s%(remote_path)s%(endpoint)s%(url_suffix)s" % { + "scheme": system.scheme or "https", + "host": system.host, + "port": ":" + str(system.port) if system.port else "", + "remote_path": system.remote_path if system.remote_path else "", + "endpoint": endpoint_record.endpoint if endpoint else "", + "url_suffix": url_suffix if url_suffix else "", + } + return url diff --git a/base_external_system_http/models/external_system_endpoint.py b/base_external_system_http/models/external_system_endpoint.py new file mode 100644 index 000000000..6f44c6915 --- /dev/null +++ b/base_external_system_http/models/external_system_endpoint.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +"""Endpoint on remote system to get from or post to.""" + +from odoo import fields, models + + +class ExternalSystemEndpoint(models.Model): + """Endpoint on remote system to get from or post to.""" + + _name = "external.system.endpoint" + _description = __doc__ + + system_id = fields.Many2one(comodel_name="external.system", required=True) + name = fields.Char(string="Logical name for endpoint", required=True) + endpoint = fields.Char( + required=True, + help="Remote path to append to base url for endpoint", + ) diff --git a/base_external_system_http/security/ir.model.access.csv b/base_external_system_http/security/ir.model.access.csv new file mode 100644 index 000000000..067c0f961 --- /dev/null +++ b/base_external_system_http/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_external_system_endpoint_admin,access_external_system_endpoint_admin,model_external_system_endpoint,base.group_system,1,1,1,1 +access_external_system_endpoint_user,access_external_system_endpoint_user,model_external_system_endpoint,base.group_user,1,0,0,0 diff --git a/base_external_system_http/static/description/icon.png b/base_external_system_http/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4c7ab302908e114888446d84d3493fa726033c1f GIT binary patch literal 1142 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r53?z4+XPOVBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpuXS$ zpAc7|0+Vy!%`Sd3J@*~Rz=H@Xz@vBMNCCrhU++~OAXQKjgnPb5^?zLm6yRy4l)f7mx|d; zx?*(k%?4)UEmyi0Ecrc2=k&YFn|8nX@qd4)(saLN%zo##oL4V9SpH%8W(I{5_Kby- zneS~VhopAyb6lBS&$U5E`gKppbdV7NQIC+SE zKe2ts8LoR*Hw$jqcIEDHSU_mi4;HoUDLTgOJMIHx zO|`@|q9i4;B-JXpC>2OC7#SEE>l#?<8d`)H8e5qfSQ#5>8yHy`7 + + + + external.system + + + + + + + + + + + + + + + + + From 0d7d5365446eab92e5fded42a24a25195c275c0a Mon Sep 17 00:00:00 2001 From: Nikos Tsirintanis Date: Fri, 16 Jan 2026 13:18:56 +0100 Subject: [PATCH 2/2] [16.0][ADD] base_external_system_http --- base_external_system_http/README.rst | 9 - base_external_system_http/__manifest__.py | 13 +- .../demo/external_system_demo.xml | 5 +- .../demo/external_system_endpoint_demo.xml | 4 +- .../models/external_system.py | 4 +- .../models/external_system_adapter_http.py | 200 +++++--- .../models/external_system_endpoint.py | 3 +- .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 8 + base_external_system_http/readme/ROADMAP.rst | 2 + base_external_system_http/readme/USAGE.rst | 24 + .../security/ir.model.access.csv | 5 +- .../static/description/index.html | 463 ++++++++++++++++++ base_external_system_http/tests/__init__.py | 5 +- .../tests/test_external_system.py | 43 -- .../tests/test_external_system_http.py | 196 ++++++++ .../odoo/addons/base_external_system_http | 1 + setup/base_external_system_http/setup.py | 6 + 18 files changed, 851 insertions(+), 142 deletions(-) mode change 100755 => 100644 base_external_system_http/demo/external_system_demo.xml create mode 100644 base_external_system_http/readme/CONTRIBUTORS.rst create mode 100644 base_external_system_http/readme/DESCRIPTION.rst create mode 100644 base_external_system_http/readme/ROADMAP.rst create mode 100644 base_external_system_http/readme/USAGE.rst create mode 100644 base_external_system_http/static/description/index.html delete mode 100644 base_external_system_http/tests/test_external_system.py create mode 100644 base_external_system_http/tests/test_external_system_http.py create mode 120000 setup/base_external_system_http/odoo/addons/base_external_system_http create mode 100644 setup/base_external_system_http/setup.py diff --git a/base_external_system_http/README.rst b/base_external_system_http/README.rst index ad3191994..e69de29bb 100644 --- a/base_external_system_http/README.rst +++ b/base_external_system_http/README.rst @@ -1,9 +0,0 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: https://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - -================== -Hc external system -================== - -A connection to external systems for Human Company. diff --git a/base_external_system_http/__manifest__.py b/base_external_system_http/__manifest__.py index 09b87a745..2df622182 100644 --- a/base_external_system_http/__manifest__.py +++ b/base_external_system_http/__manifest__.py @@ -1,20 +1,25 @@ -# Copyright 2023 Therp BV . +# Copyright 2026 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "External System HTTP", - "version": "10.0.1.0.0", + "version": "16.0.1.0.0", "author": "Therp BV, Odoo Community Association (OCA)", + "maintainers": ["NL66278", "ntsirintanis"], "license": "AGPL-3", "category": "Base", - "website": "https://github.com/OCA/server-tools", + "website": "https://github.com/OCA/server-backend", "summary": "HTTP Connector for External Systems", "depends": ["base_external_system"], "data": [ + "security/ir.model.access.csv", "demo/external_system_demo.xml", "demo/external_system_endpoint_demo.xml", - "security/ir.model.access.csv", "views/external_system.xml", ], + "demo": [ + "demo/external_system_demo.xml", + "demo/external_system_endpoint_demo.xml", + ], "installable": True, "application": False, } diff --git a/base_external_system_http/demo/external_system_demo.xml b/base_external_system_http/demo/external_system_demo.xml old mode 100755 new mode 100644 index e851931a3..9e1f73fc2 --- a/base_external_system_http/demo/external_system_demo.xml +++ b/base_external_system_http/demo/external_system_demo.xml @@ -1,6 +1,6 @@ - + @@ -8,7 +8,6 @@ Example Connection to GitHub for Testing external.system.adapter.http - https github.com /OCA diff --git a/base_external_system_http/demo/external_system_endpoint_demo.xml b/base_external_system_http/demo/external_system_endpoint_demo.xml index e6e5a664c..92de37ee2 100644 --- a/base_external_system_http/demo/external_system_endpoint_demo.xml +++ b/base_external_system_http/demo/external_system_endpoint_demo.xml @@ -1,6 +1,6 @@ - + diff --git a/base_external_system_http/models/external_system.py b/base_external_system_http/models/external_system.py index 07fbed4b7..40a685a36 100644 --- a/base_external_system_http/models/external_system.py +++ b/base_external_system_http/models/external_system.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 Therp BV . +# Copyright 2026 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -13,5 +12,6 @@ class ExternalSystem(models.Model): endpoint_ids = fields.One2many( comodel_name="external.system.endpoint", inverse_name="system_id", + string="Endpoints", help="Endpoints on remote system", ) diff --git a/base_external_system_http/models/external_system_adapter_http.py b/base_external_system_http/models/external_system_adapter_http.py index 2da76419a..5dc27cf8b 100644 --- a/base_external_system_http/models/external_system_adapter_http.py +++ b/base_external_system_http/models/external_system_adapter_http.py @@ -1,101 +1,157 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 Therp BV . +# Copyright 2026 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -"""Extend base adapter model for connections through http(s).""" -import json +"""HTTP(S) adapter for :class:`external.system`.""" + import logging import requests +from requests import exceptions as req_exc -from odoo import _, api, fields, models -from odoo.exceptions import UserError - -# You must initialize logging, otherwise you'll not see debug output. -logging.basicConfig() -logging.getLogger().setLevel(logging.DEBUG) -requests_log = logging.getLogger("requests.packages.urllib3") -requests_log.setLevel(logging.DEBUG) -requests_log.propagate = True - +from odoo import _, models +from odoo.exceptions import UserError, ValidationError -_logger = logging.getLogger(__name__) # pylint: disable=invalid-name +_logger = logging.getLogger(__name__) -class ExternalSystemAdapterHTTP(models.AbstractModel): - """HTTP external system Adapter""" +class ExternalSystemAdapterHTTP(models.Model): + """HTTP external system adapter.""" - _inherit = "external.system.adapter" _name = "external.system.adapter.http" - _description = __doc__ + _inherit = "external.system.adapter" + _description = "External System HTTP" - @api.model def external_get_client(self): - """Return self as the client.""" + self.ensure_one() return self - @api.model def external_destroy_client(self, client): - """If needed logout of server.""" - pass + self.ensure_one() + return super().external_destroy_client(client) - def get(self, endpoint=None, params=None, **kwargs): - """Pass transparantly to request.get, but check response.""" + def external_test_connection(self): + """Test connection in the UI by doing a GET on the base URL.""" + self.ensure_one() + try: + self.get(endpoint=None) + except ValidationError: + raise + except Exception as exc: + raise ValidationError(_("Connection failed.\n\nDETAIL: %s") % exc) from exc + return super().external_test_connection() + + def get(self, endpoint=None, params=None, timeout=16, **kwargs): + """GET helper.""" + self.ensure_one() + url = self._get_url(endpoint=endpoint) + _logger.debug("Will GET %s", url) + try: + response = requests.get(url, params=params, timeout=timeout, **kwargs) + except req_exc.RequestException as exc: + _logger.error("GET %s failed: %s", url, exc) + raise ValidationError( + _("GET request failed for %(url)s.\n\nDETAIL: %(detail)s") + % {"url": url, "detail": exc} + ) from exc + return self._return_checked_response(endpoint, response) + + def post(self, endpoint=None, data=None, json=None, timeout=16, **kwargs): + """POST helper.""" + self.ensure_one() url = self._get_url(endpoint=endpoint) - _logger.debug("Will get data from %s", url) - response = requests.get(url, params=params, **kwargs) - if response.status_code != 200: - message = _("Data could not be retrieved from endpoint %s: %s") % ( - endpoint, - str(response.text), + _logger.debug("Will POST %s", url) + try: + response = requests.post( + url, data=data, json=json, timeout=timeout, **kwargs ) - _logger.error(message) - raise UserError(message) - _logger.info( - _("Data succesfully retrieved from endpoint %s"), - endpoint, - ) - return response + except req_exc.RequestException as exc: + _logger.error("POST %s failed: %s", url, exc) + raise ValidationError( + _("POST request failed for %(url)s.\n\nDETAIL: %(detail)s") + % {"url": url, "detail": exc} + ) from exc + return self._return_checked_response(endpoint, response) - def post(self, endpoint=None, data=None, json=None, **kwargs): - """Post data to http server.""" + def put(self, endpoint=None, data=None, json=None, timeout=16, **kwargs): + """PUT helper.""" + self.ensure_one() url = self._get_url(endpoint=endpoint) - _logger.debug("Will post data to %s", url) - response = requests.post(url, data=data, json=json, **kwargs) - if response.status_code not in (200, 201): - message = _("Data could not be pushed to endpoint %s: %s") % ( - endpoint, - str(response.text), + _logger.debug("Will PUT %s", url) + try: + response = requests.put( + url, data=data, json=json, timeout=timeout, **kwargs ) - _logger.error(message) - raise UserError(message) + except req_exc.RequestException as exc: + _logger.error("PUT %s failed: %s", url, exc) + raise ValidationError( + _("PUT request failed for %(url)s.\n\nDETAIL: %(detail)s") + % {"url": url, "detail": exc} + ) from exc + return self._return_checked_response(endpoint, response) - def _get_url(self, endpoint=None, url_suffix=None): - """Make full url for endpoint. + def delete(self, endpoint=None, params=None, timeout=16, **kwargs): + """DELETE helper.""" + self.ensure_one() + url = self._get_url(endpoint=endpoint) + _logger.debug("Will DELETE %s", url) + try: + response = requests.delete(url, params=params, timeout=timeout, **kwargs) + except req_exc.RequestException as exc: + _logger.error("DELETE %s failed: %s", url, exc) + raise ValidationError( + _("DELETE request failed for %(url)s.\n\nDETAIL: %(detail)s") + % {"url": url, "detail": exc} + ) from exc + return self._return_checked_response(endpoint, response) - The configured remote_path, endpoint and the passed url_suffix - must, if used, always start with "/". - """ + def _get_url(self, endpoint=None, url_suffix=None): + """Build full URL for an endpoint""" + self.ensure_one() system = self.system_id + endpoint_record = None if endpoint: - endpoint_model = self.env["external.system.endpoint"] - endpoint_record = endpoint_model.search( - [ - ("system_id", "=", system.id), - ("name", "=", endpoint), - ], - limit=1 + endpoint_record = self.env["external.system.endpoint"].search( + [("system_id", "=", system.id), ("name", "=", endpoint)], + limit=1, ) if not endpoint_record: raise UserError( - _("Endpoint %s not found on system %s") - % (endpoint, system.name) + _("Endpoint %(endpoint)s not found on system %(system_name)s") + % {"endpoint": endpoint, "system_name": system.name} ) - url = "%(scheme)s://%(host)s%(port)s%(remote_path)s%(endpoint)s%(url_suffix)s" % { - "scheme": system.scheme or "https", - "host": system.host, - "port": ":" + str(system.port) if system.port else "", - "remote_path": system.remote_path if system.remote_path else "", - "endpoint": endpoint_record.endpoint if endpoint else "", - "url_suffix": url_suffix if url_suffix else "", - } - return url + host = (system.host or "").strip() + if host.startswith(("http://", "https://")): + base = host.rstrip("/") + else: + base = ("https://" + host).rstrip("/") + port = ":" + str(system.port) if system.port else "" + remote_path = (system.remote_path or "").rstrip("/") + endpoint_path = endpoint_record.endpoint if endpoint_record else "" + suffix = url_suffix or "" + return f"{base}{port}{remote_path}{endpoint_path}{suffix}" + + def _return_checked_response(self, endpoint, response): + """Validate response.""" + if response.status_code >= 400: + text = response.text or "" + _logger.error( + "Got response with statuscode %(status)s from endpoint %(endpoint)s: %(text)s", + { + "status": str(response.status_code), + "endpoint": endpoint or "", + "text": text, + }, + ) + raise ValidationError( + _( + "Communication failure with %(endpoint)s " + "(HTTP %(status)s).\n\nDETAIL: %(detail)s" + ) + % { + "endpoint": endpoint or "", + "status": response.status_code, + "detail": (text or "").strip(), + } + ) + + _logger.info("Succesfull communication with endpoint %s", endpoint or "") + return response diff --git a/base_external_system_http/models/external_system_endpoint.py b/base_external_system_http/models/external_system_endpoint.py index 6f44c6915..204a75a98 100644 --- a/base_external_system_http/models/external_system_endpoint.py +++ b/base_external_system_http/models/external_system_endpoint.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 Therp BV . +# Copyright 2026 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). """Endpoint on remote system to get from or post to.""" diff --git a/base_external_system_http/readme/CONTRIBUTORS.rst b/base_external_system_http/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..697b6daff --- /dev/null +++ b/base_external_system_http/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Ronald Portier (Therp BV) +* Nikos Tsirintanis diff --git a/base_external_system_http/readme/DESCRIPTION.rst b/base_external_system_http/readme/DESCRIPTION.rst new file mode 100644 index 000000000..06f049581 --- /dev/null +++ b/base_external_system_http/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +This module provides an HTTP(S) adapter for the ``base_external_system`` framework. + +It allows Odoo to communicate with external systems over HTTP using a consistent +adapter interface, supporting GET, POST and PUT requests, endpoint resolution, +and connection testing. + +The adapter itself acts as the client and is yielded via the standard +``system.client()`` context manager. diff --git a/base_external_system_http/readme/ROADMAP.rst b/base_external_system_http/readme/ROADMAP.rst new file mode 100644 index 000000000..e45fea246 --- /dev/null +++ b/base_external_system_http/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* Optional authentication helpers +* Optional persistent session support diff --git a/base_external_system_http/readme/USAGE.rst b/base_external_system_http/readme/USAGE.rst new file mode 100644 index 000000000..03c60f42f --- /dev/null +++ b/base_external_system_http/readme/USAGE.rst @@ -0,0 +1,24 @@ +Create an external system record and optionally define endpoints. + +Example configuration: + +* Host: ``api.example.com`` +* Remote path: ``/v1`` + +Define endpoints such as: + +* ``status`` → ``/status`` +* ``items`` → ``/items`` + +Use the adapter: + +.. code-block:: python + + with system.client() as client: + response = client.get(endpoint="status") + response = client.post(endpoint="items", json={"name": "Item"}) + response = client.put(endpoint="items", json={"active": True}) + +Responses are returned as ``requests.Response`` objects. + +Errors raise ``ValidationError`` with detailed messages. diff --git a/base_external_system_http/security/ir.model.access.csv b/base_external_system_http/security/ir.model.access.csv index 067c0f961..bd1520dea 100644 --- a/base_external_system_http/security/ir.model.access.csv +++ b/base_external_system_http/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_external_system_endpoint_admin,access_external_system_endpoint_admin,model_external_system_endpoint,base.group_system,1,1,1,1 -access_external_system_endpoint_user,access_external_system_endpoint_user,model_external_system_endpoint,base.group_user,1,0,0,0 +base_external_system_http.access_external_system_adapter_http_user,access_external_system_adapter_http_user,base_external_system_http.model_external_system_adapter_http,base.group_user,1,0,0,0 +base_external_system_http.access_external_system_endpoint_user,access_external_system_endpoint_user,base_external_system_http.model_external_system_endpoint,base.group_user,1,0,0,0 +base_external_system_http.access_external_system_endpoint_admin,access_external_system_endpoint_admin,base_external_system_http.model_external_system_endpoint,base.group_system,1,1,1,1 diff --git a/base_external_system_http/static/description/index.html b/base_external_system_http/static/description/index.html new file mode 100644 index 000000000..fdc18fe09 --- /dev/null +++ b/base_external_system_http/static/description/index.html @@ -0,0 +1,463 @@ + + + + + +External System HTTP + + + +
+

External System HTTP

+ + +

Beta License: AGPL-3 OCA/server-backend Translate me on Weblate Try me on Runboat

+

This module provides an HTTP(S) adapter for the base_external_system framework.

+

It allows Odoo to communicate with external systems over HTTP using a consistent +adapter interface, supporting GET, POST and PUT requests, endpoint resolution, +and connection testing.

+

The adapter itself acts as the client and is yielded via the standard +system.client() context manager.

+

Table of contents

+ +
+

Usage

+

Create an external system record and optionally define endpoints.

+

Example configuration:

+
    +
  • Host: api.example.com
  • +
  • Remote path: /v1
  • +
+

Define endpoints such as:

+
    +
  • status/status
  • +
  • items/items
  • +
+

Use the adapter:

+
+with system.client() as client:
+    response = client.get(endpoint="status")
+    response = client.post(endpoint="items", json={"name": "Item"})
+    response = client.put(endpoint="items", json={"active": True})
+
+

Responses are returned as requests.Response objects.

+

Errors raise ValidationError with detailed messages.

+
+
+

Known issues / Roadmap

+
    +
  • Optional authentication helpers
  • +
  • Optional persistent session support
  • +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

NL66278 ntsirintanis

+

This module is part of the OCA/server-backend project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_external_system_http/tests/__init__.py b/base_external_system_http/tests/__init__.py index 633c53410..dbb83aed8 100644 --- a/base_external_system_http/tests/__init__.py +++ b/base_external_system_http/tests/__init__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- - -from . import test_external_system +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import test_external_system_http diff --git a/base_external_system_http/tests/test_external_system.py b/base_external_system_http/tests/test_external_system.py deleted file mode 100644 index ad79a589b..000000000 --- a/base_external_system_http/tests/test_external_system.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 Therp BV. -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.exceptions import UserError, ValidationError -from odoo.tests.common import TransactionCase - - -class TestExternalSystem(TransactionCase): - def setUp(self): - super(TestExternalSystem, self).setUp() - self.record = self.env.ref("base_external_system_http.external_system_github") - - def test_get_system_types(self): - """It should return at least the test record's interface.""" - system_type_http = self.env["external.system.adapter.http"] - self.assertIn( - (system_type_http._name, system_type_http._description), - self.env["external.system"]._get_system_types(), - ) - - def test_client(self): - """The client should be the adapter class.""" - system_type_http = self.env["external.system.adapter.http"] - with self.record.client() as client: - self.assertEqual(client, system_type_http) - # Client should have system_id property. - self.assertEqual(client.system_id, self.record) - - def test_get_http(self): - """We should be able to get response from system.""" - with self.record.client() as client: - response = client.get() - self.assertIn("", response.text) + + def test_get_with_endpoint(self): + with patch.object(http_mod.requests, "get") as mock_get: + mock_get.return_value = _FakeResponse(status_code=200, text="server-tools") + + with self.system.client() as client: + response = client.get(endpoint="tools") + + called_url = mock_get.call_args[0][0] + self.assertIn("/server-tools", called_url) + self.assertIn("server-tools", response.text) + + def test_get_http_error(self): + with patch.object(http_mod.requests, "get") as mock_get: + mock_get.return_value = _FakeResponse(status_code=500, text="boom") + with self.assertRaises(ValidationError): + with self.system.client() as client: + client.get() + + def test_get_request_exception(self): + with patch.object( + http_mod.requests, + "get", + side_effect=http_mod.req_exc.RequestException("network down"), + ): + with self.assertRaises(ValidationError): + with self.system.client() as client: + client.get() + + def test_post_response(self): + with patch.object(http_mod.requests, "post") as mock_post: + mock_post.return_value = _FakeResponse(status_code=200, text="ok") + with self.system.client() as client: + response = client.post() + mock_post.assert_called() + called_url = mock_post.call_args[0][0] + self.assertTrue(called_url.startswith("https://github.com")) + self.assertIn("/OCA", called_url) + self.assertIn("ok", response.text) + + def test_post_with_endpoint(self): + with patch.object(http_mod.requests, "post") as mock_post: + mock_post.return_value = _FakeResponse(status_code=200, text="server-tools") + with self.system.client() as client: + response = client.post(endpoint="tools") + called_url = mock_post.call_args[0][0] + self.assertIn("/server-tools", called_url) + self.assertIn("server-tools", response.text) + + def test_post_http_error(self): + with patch.object(http_mod.requests, "post") as mock_post: + mock_post.return_value = _FakeResponse(status_code=500, text="boom") + with self.assertRaises(ValidationError): + with self.system.client() as client: + client.post() + + def test_put_response(self): + with patch.object(http_mod.requests, "put") as mock_put: + mock_put.return_value = _FakeResponse(status_code=200, text="ok") + + with self.system.client() as client: + response = client.put() + mock_put.assert_called() + called_url = mock_put.call_args[0][0] + self.assertTrue(called_url.startswith("https://github.com")) + self.assertIn("/OCA", called_url) + self.assertIn("ok", response.text) + + def test_put_with_endpoint(self): + with patch.object(http_mod.requests, "put") as mock_put: + mock_put.return_value = _FakeResponse(status_code=200, text="server-tools") + with self.system.client() as client: + response = client.put(endpoint="tools") + called_url = mock_put.call_args[0][0] + self.assertIn("/server-tools", called_url) + self.assertIn("server-tools", response.text) + + def test_put_http_error(self): + with patch.object(http_mod.requests, "put") as mock_put: + mock_put.return_value = _FakeResponse(status_code=500, text="boom") + with self.assertRaises(ValidationError): + with self.system.client() as client: + client.put() + + def test_delete_response(self): + with patch.object(http_mod.requests, "delete") as mock_delete: + mock_delete.return_value = _FakeResponse(status_code=200, text="ok") + with self.system.client() as client: + response = client.delete() + mock_delete.assert_called() + called_url = mock_delete.call_args[0][0] + self.assertTrue(called_url.startswith("https://github.com")) + self.assertIn("/OCA", called_url) + self.assertIn("ok", response.text) + + def test_delete_with_endpoint(self): + with patch.object(http_mod.requests, "delete") as mock_delete: + mock_delete.return_value = _FakeResponse( + status_code=200, text="server-tools" + ) + with self.system.client() as client: + response = client.delete(endpoint="tools") + called_url = mock_delete.call_args[0][0] + self.assertIn("/server-tools", called_url) + self.assertIn("server-tools", response.text) + + def test_delete_http_error(self): + with patch.object(http_mod.requests, "delete") as mock_delete: + mock_delete.return_value = _FakeResponse(status_code=500, text="boom") + with self.assertRaises(ValidationError): + with self.system.client() as client: + client.delete() + + def test_get_url(self): + sys = self._make_variant_system( + name="HTTP Test System (https host)", + host="https://github.com", + ) + with sys.client() as client: + url = client._get_url() + self.assertTrue(url.startswith("https://github.com")) + sys2 = self._make_variant_system( + name="HTTP Test System (http host)", + host="http://github.com", + ) + with sys2.client() as client: + url2 = client._get_url() + self.assertTrue(url2.startswith("http://github.com")) + + def test_action_test_connection(self): + with patch.object(http_mod.requests, "get") as mock_get: + mock_get.return_value = _FakeResponse(status_code=200, text="ok") + with self.assertRaises(UserError): + self.system.action_test_connection() diff --git a/setup/base_external_system_http/odoo/addons/base_external_system_http b/setup/base_external_system_http/odoo/addons/base_external_system_http new file mode 120000 index 000000000..1a3fdb337 --- /dev/null +++ b/setup/base_external_system_http/odoo/addons/base_external_system_http @@ -0,0 +1 @@ +../../../../base_external_system_http \ No newline at end of file diff --git a/setup/base_external_system_http/setup.py b/setup/base_external_system_http/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/base_external_system_http/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)