Skip to content

Commit 4c15177

Browse files
committed
[16.0][ADD] base_external_system_http
1 parent 01e9d96 commit 4c15177

18 files changed

Lines changed: 851 additions & 142 deletions

base_external_system_http/README.rst

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
# Copyright 2023 Therp BV <https://therp.nl>.
1+
# Copyright 2026 Therp BV <https://therp.nl>.
22
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
33
{
44
"name": "External System HTTP",
5-
"version": "10.0.1.0.0",
5+
"version": "16.0.1.0.0",
66
"author": "Therp BV, Odoo Community Association (OCA)",
7+
"maintainers": ["NL66278", "ntsirintanis"],
78
"license": "AGPL-3",
89
"category": "Base",
9-
"website": "https://github.com/OCA/server-tools",
10+
"website": "https://github.com/OCA/server-backend",
1011
"summary": "HTTP Connector for External Systems",
1112
"depends": ["base_external_system"],
1213
"data": [
14+
"security/ir.model.access.csv",
1315
"demo/external_system_demo.xml",
1416
"demo/external_system_endpoint_demo.xml",
15-
"security/ir.model.access.csv",
1617
"views/external_system.xml",
1718
],
19+
"demo": [
20+
"demo/external_system_demo.xml",
21+
"demo/external_system_endpoint_demo.xml",
22+
],
1823
"installable": True,
1924
"application": False,
2025
}

base_external_system_http/demo/external_system_demo.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8" ?>
22
<!--
3-
Copyright 2023 Therp BV.
3+
Copyright 2026 Therp BV.
44
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
55
-->
66
<odoo>
77

88
<record id="external_system_github" model="external.system">
99
<field name="name">Example Connection to GitHub for Testing</field>
1010
<field name="system_type">external.system.adapter.http</field>
11-
<field name="scheme">https</field>
1211
<field name="host">github.com</field>
1312
<field name="remote_path">/OCA</field>
1413
<field name="company_ids" eval="[(5, 0), (4, ref('base.main_company'))]" />

base_external_system_http/demo/external_system_endpoint_demo.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8" ?>
22
<!--
3-
Copyright 2023 Therp BV.
3+
Copyright 2026 Therp BV.
44
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
55
-->
66
<odoo>

base_external_system_http/models/external_system.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# -*- coding: utf-8 -*-
2-
# Copyright 2023 Therp BV <https://therp.nl>.
1+
# Copyright 2026 Therp BV <https://therp.nl>.
32
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
43

54
from odoo import fields, models
@@ -13,5 +12,6 @@ class ExternalSystem(models.Model):
1312
endpoint_ids = fields.One2many(
1413
comodel_name="external.system.endpoint",
1514
inverse_name="system_id",
15+
string="Endpoints",
1616
help="Endpoints on remote system",
1717
)
Lines changed: 128 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,157 @@
1-
# -*- coding: utf-8 -*-
2-
# Copyright 2023 Therp BV <https://therp.nl>.
1+
# Copyright 2026 Therp BV <https://therp.nl>.
32
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
4-
"""Extend base adapter model for connections through http(s)."""
5-
import json
3+
"""HTTP(S) adapter for :class:`external.system`."""
4+
65
import logging
76

87
import requests
8+
from requests import exceptions as req_exc
99

10-
from odoo import _, api, fields, models
11-
from odoo.exceptions import UserError
12-
13-
# You must initialize logging, otherwise you'll not see debug output.
14-
logging.basicConfig()
15-
logging.getLogger().setLevel(logging.DEBUG)
16-
requests_log = logging.getLogger("requests.packages.urllib3")
17-
requests_log.setLevel(logging.DEBUG)
18-
requests_log.propagate = True
19-
10+
from odoo import _, models
11+
from odoo.exceptions import UserError, ValidationError
2012

21-
_logger = logging.getLogger(__name__) # pylint: disable=invalid-name
13+
_logger = logging.getLogger(__name__)
2214

2315

24-
class ExternalSystemAdapterHTTP(models.AbstractModel):
25-
"""HTTP external system Adapter"""
16+
class ExternalSystemAdapterHTTP(models.Model):
17+
"""HTTP external system adapter."""
2618

27-
_inherit = "external.system.adapter"
2819
_name = "external.system.adapter.http"
29-
_description = __doc__
20+
_inherit = "external.system.adapter"
21+
_description = "External System HTTP"
3022

31-
@api.model
3223
def external_get_client(self):
33-
"""Return self as the client."""
24+
self.ensure_one()
3425
return self
3526

36-
@api.model
3727
def external_destroy_client(self, client):
38-
"""If needed logout of server."""
39-
pass
28+
self.ensure_one()
29+
return super().external_destroy_client(client)
4030

41-
def get(self, endpoint=None, params=None, **kwargs):
42-
"""Pass transparantly to request.get, but check response."""
31+
def external_test_connection(self):
32+
"""Test connection in the UI by doing a GET on the base URL."""
33+
self.ensure_one()
34+
try:
35+
self.get(endpoint=None)
36+
except ValidationError:
37+
raise
38+
except Exception as exc:
39+
raise ValidationError(_("Connection failed.\n\nDETAIL: %s") % exc) from exc
40+
return super().external_test_connection()
41+
42+
def get(self, endpoint=None, params=None, timeout=16, **kwargs):
43+
"""GET helper."""
44+
self.ensure_one()
45+
url = self._get_url(endpoint=endpoint)
46+
_logger.debug("Will GET %s", url)
47+
try:
48+
response = requests.get(url, params=params, timeout=timeout, **kwargs)
49+
except req_exc.RequestException as exc:
50+
_logger.error("GET %s failed: %s", url, exc)
51+
raise ValidationError(
52+
_("GET request failed for %(url)s.\n\nDETAIL: %(detail)s")
53+
% {"url": url, "detail": exc}
54+
) from exc
55+
return self._return_checked_response(endpoint, response)
56+
57+
def post(self, endpoint=None, data=None, json=None, timeout=16, **kwargs):
58+
"""POST helper."""
59+
self.ensure_one()
4360
url = self._get_url(endpoint=endpoint)
44-
_logger.debug("Will get data from %s", url)
45-
response = requests.get(url, params=params, **kwargs)
46-
if response.status_code != 200:
47-
message = _("Data could not be retrieved from endpoint %s: %s") % (
48-
endpoint,
49-
str(response.text),
61+
_logger.debug("Will POST %s", url)
62+
try:
63+
response = requests.post(
64+
url, data=data, json=json, timeout=timeout, **kwargs
5065
)
51-
_logger.error(message)
52-
raise UserError(message)
53-
_logger.info(
54-
_("Data succesfully retrieved from endpoint %s"),
55-
endpoint,
56-
)
57-
return response
66+
except req_exc.RequestException as exc:
67+
_logger.error("POST %s failed: %s", url, exc)
68+
raise ValidationError(
69+
_("POST request failed for %(url)s.\n\nDETAIL: %(detail)s")
70+
% {"url": url, "detail": exc}
71+
) from exc
72+
return self._return_checked_response(endpoint, response)
5873

59-
def post(self, endpoint=None, data=None, json=None, **kwargs):
60-
"""Post data to http server."""
74+
def put(self, endpoint=None, data=None, json=None, timeout=16, **kwargs):
75+
"""PUT helper."""
76+
self.ensure_one()
6177
url = self._get_url(endpoint=endpoint)
62-
_logger.debug("Will post data to %s", url)
63-
response = requests.post(url, data=data, json=json, **kwargs)
64-
if response.status_code not in (200, 201):
65-
message = _("Data could not be pushed to endpoint %s: %s") % (
66-
endpoint,
67-
str(response.text),
78+
_logger.debug("Will PUT %s", url)
79+
try:
80+
response = requests.put(
81+
url, data=data, json=json, timeout=timeout, **kwargs
6882
)
69-
_logger.error(message)
70-
raise UserError(message)
83+
except req_exc.RequestException as exc:
84+
_logger.error("PUT %s failed: %s", url, exc)
85+
raise ValidationError(
86+
_("PUT request failed for %(url)s.\n\nDETAIL: %(detail)s")
87+
% {"url": url, "detail": exc}
88+
) from exc
89+
return self._return_checked_response(endpoint, response)
7190

72-
def _get_url(self, endpoint=None, url_suffix=None):
73-
"""Make full url for endpoint.
91+
def delete(self, endpoint=None, params=None, timeout=16, **kwargs):
92+
"""DELETE helper."""
93+
self.ensure_one()
94+
url = self._get_url(endpoint=endpoint)
95+
_logger.debug("Will DELETE %s", url)
96+
try:
97+
response = requests.delete(url, params=params, timeout=timeout, **kwargs)
98+
except req_exc.RequestException as exc:
99+
_logger.error("DELETE %s failed: %s", url, exc)
100+
raise ValidationError(
101+
_("DELETE request failed for %(url)s.\n\nDETAIL: %(detail)s")
102+
% {"url": url, "detail": exc}
103+
) from exc
104+
return self._return_checked_response(endpoint, response)
74105

75-
The configured remote_path, endpoint and the passed url_suffix
76-
must, if used, always start with "/".
77-
"""
106+
def _get_url(self, endpoint=None, url_suffix=None):
107+
"""Build full URL for an endpoint"""
108+
self.ensure_one()
78109
system = self.system_id
110+
endpoint_record = None
79111
if endpoint:
80-
endpoint_model = self.env["external.system.endpoint"]
81-
endpoint_record = endpoint_model.search(
82-
[
83-
("system_id", "=", system.id),
84-
("name", "=", endpoint),
85-
],
86-
limit=1
112+
endpoint_record = self.env["external.system.endpoint"].search(
113+
[("system_id", "=", system.id), ("name", "=", endpoint)],
114+
limit=1,
87115
)
88116
if not endpoint_record:
89117
raise UserError(
90-
_("Endpoint %s not found on system %s")
91-
% (endpoint, system.name)
118+
_("Endpoint %(endpoint)s not found on system %(system_name)s")
119+
% {"endpoint": endpoint, "system_name": system.name}
92120
)
93-
url = "%(scheme)s://%(host)s%(port)s%(remote_path)s%(endpoint)s%(url_suffix)s" % {
94-
"scheme": system.scheme or "https",
95-
"host": system.host,
96-
"port": ":" + str(system.port) if system.port else "",
97-
"remote_path": system.remote_path if system.remote_path else "",
98-
"endpoint": endpoint_record.endpoint if endpoint else "",
99-
"url_suffix": url_suffix if url_suffix else "",
100-
}
101-
return url
121+
host = (system.host or "").strip()
122+
if host.startswith(("http://", "https://")):
123+
base = host.rstrip("/")
124+
else:
125+
base = ("https://" + host).rstrip("/")
126+
port = ":" + str(system.port) if system.port else ""
127+
remote_path = (system.remote_path or "").rstrip("/")
128+
endpoint_path = endpoint_record.endpoint if endpoint_record else ""
129+
suffix = url_suffix or ""
130+
return f"{base}{port}{remote_path}{endpoint_path}{suffix}"
131+
132+
def _return_checked_response(self, endpoint, response):
133+
"""Validate response."""
134+
if response.status_code >= 400:
135+
text = response.text or ""
136+
_logger.error(
137+
"Got response with statuscode %(status)s from endpoint %(endpoint)s: %(text)s",
138+
{
139+
"status": str(response.status_code),
140+
"endpoint": endpoint or "<base>",
141+
"text": text,
142+
},
143+
)
144+
raise ValidationError(
145+
_(
146+
"Communication failure with %(endpoint)s "
147+
"(HTTP %(status)s).\n\nDETAIL: %(detail)s"
148+
)
149+
% {
150+
"endpoint": endpoint or "<base>",
151+
"status": response.status_code,
152+
"detail": (text or "").strip(),
153+
}
154+
)
155+
156+
_logger.info("Succesfull communication with endpoint %s", endpoint or "<base>")
157+
return response

base_external_system_http/models/external_system_endpoint.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# -*- coding: utf-8 -*-
2-
# Copyright 2023 Therp BV <https://therp.nl>.
1+
# Copyright 2026 Therp BV <https://therp.nl>.
32
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
43
"""Endpoint on remote system to get from or post to."""
54

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* Ronald Portier (Therp BV)
2+
* Nikos Tsirintanis <ntsirintanis@therp.nl>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
This module provides an HTTP(S) adapter for the ``base_external_system`` framework.
2+
3+
It allows Odoo to communicate with external systems over HTTP using a consistent
4+
adapter interface, supporting GET, POST and PUT requests, endpoint resolution,
5+
and connection testing.
6+
7+
The adapter itself acts as the client and is yielded via the standard
8+
``system.client()`` context manager.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* Optional authentication helpers
2+
* Optional persistent session support

0 commit comments

Comments
 (0)