From f8c103a29e2d88c8ffbba8e4df00adffa99f9c46 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 26 Jun 2024 16:59:25 +0200 Subject: [PATCH 1/7] [FIX] fastapi: Avoid process stuck in case of retry of Post request with body content. In case of retry we must ensure that the stream pass to the Fastapi application is reset to the beginning to be sure it can be consumed again. Unfortunately , the stream object from the werkzeug request is not always seekable. In such a case, we wrap the stream into a new SeekableStream object that it become possible to reset the stream at the begining without having to read the stream first into memory. --- fastapi/fastapi_dispatcher.py | 18 ++++- fastapi/readme/newsfragments/440.bugfix | 6 ++ fastapi/routers/demo_router.py | 27 ++++++- fastapi/seekable_stream.py | 101 ++++++++++++++++++++++++ fastapi/tests/__init__.py | 1 + fastapi/tests/test_fastapi.py | 12 +++ fastapi/tests/test_seekable_stream.py | 86 ++++++++++++++++++++ 7 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 fastapi/readme/newsfragments/440.bugfix create mode 100644 fastapi/seekable_stream.py create mode 100644 fastapi/tests/test_seekable_stream.py diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index dda4b20e4..e654c8b1e 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -16,6 +16,7 @@ from fastapi.utils import is_body_allowed_for_status_code from .context import odoo_env_ctx +from .seekable_stream import SeekableStream class FastApiDispatcher(Dispatcher): @@ -112,7 +113,22 @@ def _get_environ(self): # date odoo version. (EAFP: Easier to Ask for Forgiveness than Permission) httprequest = self.request.httprequest environ = httprequest.environ - environ["wsgi.input"] = httprequest._get_stream_for_parsing() + stream = httprequest._get_stream_for_parsing() + # Check if the stream supports seeking + if hasattr(stream, "seekable") and stream.seekable(): + # Reset the stream to the beginning to ensure it can be consumed + # again by the application in case of a retry mechanism + stream.seek(0) + else: + # If the stream does not support seeking, we need wrap it + # in a SeekableStream object that will buffer the data read + # from the stream. This way we can seek back to the beginning + # of the stream to read the data again if needed. + if not hasattr(httprequest, "_cached_stream"): + httprequest._cached_stream = SeekableStream(stream) + stream = httprequest._cached_stream + stream.seek(0) + environ["wsgi.input"] = stream return environ @contextmanager diff --git a/fastapi/readme/newsfragments/440.bugfix b/fastapi/readme/newsfragments/440.bugfix new file mode 100644 index 000000000..202669d47 --- /dev/null +++ b/fastapi/readme/newsfragments/440.bugfix @@ -0,0 +1,6 @@ +Fix issue with the retry of a POST request with a body content. + +Prior to this fix the retry of a POST request with a body content would +stuck in a loop and never complete. This was due to the fact that the +request input stream was not reset after a failed attempt to process the +request. diff --git a/fastapi/routers/demo_router.py b/fastapi/routers/demo_router.py index e316d8dfb..01e9eef1d 100644 --- a/fastapi/routers/demo_router.py +++ b/fastapi/routers/demo_router.py @@ -15,7 +15,8 @@ from odoo.addons.base.models.res_partner import Partner -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, File, HTTPException, Query, status +from fastapi.responses import JSONResponse from ..dependencies import authenticated_partner, fastapi_endpoint, odoo_env from ..models import FastapiEndpoint @@ -95,7 +96,7 @@ async def endpoint_app_info( @router.get("/demo/retrying") async def retrying( - nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)] + nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)], ) -> int: """This method is used in the test suite to check that the retrying functionality in case of concurrency error on the database is working @@ -114,6 +115,28 @@ async def retrying( return tryno +@router.post("/demo/retrying") +async def retrying_post( + nbr_retries: Annotated[int, Query(gt=1, lt=MAX_TRIES_ON_CONCURRENCY_FAILURE)], + file: Annotated[bytes, File()], +) -> JSONResponse: + """This method is used in the test suite to check that the retrying + functionality in case of concurrency error on the database is working + correctly for retryable exceptions. + + The output will be the number of retries that have been done. + + This method is mainly used to test the retrying functionality + """ + global _CPT + if _CPT < nbr_retries: + _CPT += 1 + raise FakeConcurrentUpdateError("fake error") + tryno = _CPT + _CPT = 0 + return JSONResponse(content={"retries": tryno, "file": file.decode("utf-8")}) + + class FakeConcurrentUpdateError(OperationalError): @property def pgcode(self): diff --git a/fastapi/seekable_stream.py b/fastapi/seekable_stream.py new file mode 100644 index 000000000..18de56708 --- /dev/null +++ b/fastapi/seekable_stream.py @@ -0,0 +1,101 @@ +# Copyright 2024 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). +import io + + +class SeekableStream(io.RawIOBase): + """A seekable stream that wraps another stream and buffers read data. + + This class allows to seek and read data from the original stream. + It buffers read data to allow seeking back to read data again. + + This class is useful to handle the case where the original stream does not + support seeking, but the data could eventually be read multiple times. + To avoid reading the original stream a first time to buffer the data, we + buffer the data as it is read. In this way we do not add delay when the + data is read only once. + """ + + def __init__(self, original_stream): + super().__init__() + self.original_stream = original_stream + self.buffer = bytearray() + self.buffer_position = 0 + self.seek_position = 0 + self.end_of_stream = False + + def read(self, size=-1): # pylint: disable=method-required-super + if size == -1: + # Read all remaining data + size = len(self.buffer) - self.buffer_position + data_from_buffer = bytes(self.buffer[self.buffer_position :]) + self.buffer_position = len(self.buffer) + + # Read remaining data from the original stream if not already buffered + remaining_data = self.original_stream.read() + self.buffer.extend(remaining_data) + self.end_of_stream = True + return data_from_buffer + remaining_data + + buffer_len = len(self.buffer) + remaining_buffer = buffer_len - self.buffer_position + + if remaining_buffer >= size: + # Read from the buffer if there is enough data + data = self.buffer[self.buffer_position : self.buffer_position + size] + self.buffer_position += size + return bytes(data) + else: + # Read remaining buffer data + data = self.buffer[self.buffer_position :] + self.buffer_position = buffer_len + + # Read the rest from the original stream + additional_data = self.original_stream.read(size - remaining_buffer) + if additional_data is None: + additional_data = b"" + + # Store read data in the buffer + self.buffer.extend(additional_data) + self.buffer_position += len(additional_data) + if len(additional_data) < (size - remaining_buffer): + self.end_of_stream = True + return bytes(data + additional_data) + + def seek(self, offset, whence=io.SEEK_SET): + if whence == io.SEEK_SET: + new_position = offset + elif whence == io.SEEK_CUR: + new_position = self.buffer_position + offset + elif whence == io.SEEK_END: + if not self.end_of_stream: + # Read the rest of the stream to buffer it + # This is needed to know the total size of the stream + self.read() + new_position = len(self.buffer) + offset + + if new_position < 0: + raise ValueError("Negative seek position {}".format(new_position)) + + if new_position <= len(self.buffer): + self.buffer_position = new_position + else: + # Read from the original stream to fill the buffer up to the new position + to_read = new_position - len(self.buffer) + additional_data = self.original_stream.read(to_read) + if additional_data is None: + additional_data = b"" + self.buffer.extend(additional_data) + if len(self.buffer) < new_position: + raise io.UnsupportedOperation( + "Cannot seek beyond the end of the stream" + ) + self.buffer_position = new_position + + return self.buffer_position + + def tell(self): + return self.buffer_position + + def readable(self): + return True diff --git a/fastapi/tests/__init__.py b/fastapi/tests/__init__.py index ea40c354c..644c83e60 100644 --- a/fastapi/tests/__init__.py +++ b/fastapi/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_fastapi from . import test_fastapi_demo +from . import test_seekable_stream diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index dcc7385b6..37b11a961 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -63,6 +63,18 @@ def test_retrying(self): self.assertEqual(response.status_code, 200) self.assertEqual(int(response.content), nbr_retries) + def test_retrying_post(self): + """Test that the retrying mechanism is working as expected with the + FastAPI endpoints in case of POST request with a file. + """ + nbr_retries = 3 + route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}" + response = self.url_open( + route, timeout=20, files={"file": ("test.txt", b"test")} + ) + self.assertEqual(response.status_code, 200) + self.assertDictEqual(response.json(), {"retries": nbr_retries, "file": "test"}) + @mute_logger("odoo.http") def assert_exception_processed( self, diff --git a/fastapi/tests/test_seekable_stream.py b/fastapi/tests/test_seekable_stream.py new file mode 100644 index 000000000..12001b36e --- /dev/null +++ b/fastapi/tests/test_seekable_stream.py @@ -0,0 +1,86 @@ +import io +import random + +from odoo.tests.common import TransactionCase + +from ..seekable_stream import SeekableStream + + +class TestSeekableStream(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # create a random large content + cls.original_content = random.randbytes(1024 * 1024) + + def setUp(self): + super().setUp() + self.original_stream = NonSeekableStream(self.original_content) + + def test_read_all(self): + self.assertFalse(self.original_stream.seekable()) + stream = SeekableStream(self.original_stream) + data = stream.read() + self.assertEqual(data, self.original_content) + stream.seek(0) + data = stream.read() + self.assertEqual(data, self.original_content) + + def test_read_partial(self): + self.assertFalse(self.original_stream.seekable()) + stream = SeekableStream(self.original_stream) + data = stream.read(10) + self.assertEqual(data, self.original_content[:10]) + data = stream.read(10) + self.assertEqual(data, self.original_content[10:20]) + # read the rest + data = stream.read() + self.assertEqual(data, self.original_content[20:]) + + def test_seek(self): + self.assertFalse(self.original_stream.seekable()) + stream = SeekableStream(self.original_stream) + stream.seek(10) + self.assertEqual(stream.tell(), 10) + data = stream.read(10) + self.assertEqual(data, self.original_content[10:20]) + stream.seek(0) + self.assertEqual(stream.tell(), 0) + data = stream.read(10) + self.assertEqual(data, self.original_content[:10]) + + def test_seek_relative(self): + self.assertFalse(self.original_stream.seekable()) + stream = SeekableStream(self.original_stream) + stream.seek(10) + self.assertEqual(stream.tell(), 10) + stream.seek(5, io.SEEK_CUR) + self.assertEqual(stream.tell(), 15) + data = stream.read(10) + self.assertEqual(data, self.original_content[15:25]) + + def test_seek_end(self): + self.assertFalse(self.original_stream.seekable()) + stream = SeekableStream(self.original_stream) + stream.seek(-10, io.SEEK_END) + self.assertEqual(stream.tell(), len(self.original_content) - 10) + data = stream.read(10) + self.assertEqual(data, self.original_content[-10:]) + stream.seek(0, io.SEEK_END) + self.assertEqual(stream.tell(), len(self.original_content)) + data = stream.read(10) + self.assertEqual(data, b"") + stream.seek(-len(self.original_content), io.SEEK_END) + self.assertEqual(stream.tell(), 0) + data = stream.read(10) + + +class NonSeekableStream(io.BytesIO): + def seekable(self): + return False + + def seek(self, offset, whence=io.SEEK_SET): + raise io.UnsupportedOperation("seek") + + def tell(self): + raise io.UnsupportedOperation("tell") From 89e9e14b6f810b729a2f3ccadc73b1637f973b36 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 27 Jun 2024 10:22:06 +0200 Subject: [PATCH 2/7] [FIX] fastapi: Fix minimal version for ext dependencies These minimal versions ensure that the retrying mechanism from odoo is working fine with the way the werkezeug request is pass from odoo to the fastapi app. --- fastapi/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index c983c9a1c..150af98c8 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -22,10 +22,10 @@ "demo": ["demo/fastapi_endpoint_demo.xml"], "external_dependencies": { "python": [ - "fastapi", + "fastapi>=0.110.0", "python-multipart", "ujson", - "a2wsgi", + "a2wsgi>=1.10.6", "parse-accept-language", ] }, From 05d501282922b6ebf5a6e55dfe1295a189820e41 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 28 Jun 2024 12:10:02 +0200 Subject: [PATCH 3/7] [IMP] fastapi: Simplified code and improved perfs The use of a BytesIO in place of our specialized SeekableStream class to keep the input stream in case we need to process the request again due to a retryable error outperforms both in terms of speed and memory consumption. see https://github.com/OCA/rest-framework/pull/440#issuecomment-2196564775 for more info. --- fastapi/fastapi_dispatcher.py | 6 +- fastapi/seekable_stream.py | 101 -------------------------- fastapi/tests/__init__.py | 1 - fastapi/tests/test_seekable_stream.py | 86 ---------------------- 4 files changed, 2 insertions(+), 192 deletions(-) delete mode 100644 fastapi/seekable_stream.py delete mode 100644 fastapi/tests/test_seekable_stream.py diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index e654c8b1e..8bd9aa412 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -16,7 +16,6 @@ from fastapi.utils import is_body_allowed_for_status_code from .context import odoo_env_ctx -from .seekable_stream import SeekableStream class FastApiDispatcher(Dispatcher): @@ -121,11 +120,10 @@ def _get_environ(self): stream.seek(0) else: # If the stream does not support seeking, we need wrap it - # in a SeekableStream object that will buffer the data read - # from the stream. This way we can seek back to the beginning + # in a BytesIO object. This way we can seek back to the beginning # of the stream to read the data again if needed. if not hasattr(httprequest, "_cached_stream"): - httprequest._cached_stream = SeekableStream(stream) + httprequest._cached_stream = BytesIO(stream.read()) stream = httprequest._cached_stream stream.seek(0) environ["wsgi.input"] = stream diff --git a/fastapi/seekable_stream.py b/fastapi/seekable_stream.py deleted file mode 100644 index 18de56708..000000000 --- a/fastapi/seekable_stream.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2024 ACSONE SA/NV -# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -import io - - -class SeekableStream(io.RawIOBase): - """A seekable stream that wraps another stream and buffers read data. - - This class allows to seek and read data from the original stream. - It buffers read data to allow seeking back to read data again. - - This class is useful to handle the case where the original stream does not - support seeking, but the data could eventually be read multiple times. - To avoid reading the original stream a first time to buffer the data, we - buffer the data as it is read. In this way we do not add delay when the - data is read only once. - """ - - def __init__(self, original_stream): - super().__init__() - self.original_stream = original_stream - self.buffer = bytearray() - self.buffer_position = 0 - self.seek_position = 0 - self.end_of_stream = False - - def read(self, size=-1): # pylint: disable=method-required-super - if size == -1: - # Read all remaining data - size = len(self.buffer) - self.buffer_position - data_from_buffer = bytes(self.buffer[self.buffer_position :]) - self.buffer_position = len(self.buffer) - - # Read remaining data from the original stream if not already buffered - remaining_data = self.original_stream.read() - self.buffer.extend(remaining_data) - self.end_of_stream = True - return data_from_buffer + remaining_data - - buffer_len = len(self.buffer) - remaining_buffer = buffer_len - self.buffer_position - - if remaining_buffer >= size: - # Read from the buffer if there is enough data - data = self.buffer[self.buffer_position : self.buffer_position + size] - self.buffer_position += size - return bytes(data) - else: - # Read remaining buffer data - data = self.buffer[self.buffer_position :] - self.buffer_position = buffer_len - - # Read the rest from the original stream - additional_data = self.original_stream.read(size - remaining_buffer) - if additional_data is None: - additional_data = b"" - - # Store read data in the buffer - self.buffer.extend(additional_data) - self.buffer_position += len(additional_data) - if len(additional_data) < (size - remaining_buffer): - self.end_of_stream = True - return bytes(data + additional_data) - - def seek(self, offset, whence=io.SEEK_SET): - if whence == io.SEEK_SET: - new_position = offset - elif whence == io.SEEK_CUR: - new_position = self.buffer_position + offset - elif whence == io.SEEK_END: - if not self.end_of_stream: - # Read the rest of the stream to buffer it - # This is needed to know the total size of the stream - self.read() - new_position = len(self.buffer) + offset - - if new_position < 0: - raise ValueError("Negative seek position {}".format(new_position)) - - if new_position <= len(self.buffer): - self.buffer_position = new_position - else: - # Read from the original stream to fill the buffer up to the new position - to_read = new_position - len(self.buffer) - additional_data = self.original_stream.read(to_read) - if additional_data is None: - additional_data = b"" - self.buffer.extend(additional_data) - if len(self.buffer) < new_position: - raise io.UnsupportedOperation( - "Cannot seek beyond the end of the stream" - ) - self.buffer_position = new_position - - return self.buffer_position - - def tell(self): - return self.buffer_position - - def readable(self): - return True diff --git a/fastapi/tests/__init__.py b/fastapi/tests/__init__.py index 644c83e60..ea40c354c 100644 --- a/fastapi/tests/__init__.py +++ b/fastapi/tests/__init__.py @@ -1,3 +1,2 @@ from . import test_fastapi from . import test_fastapi_demo -from . import test_seekable_stream diff --git a/fastapi/tests/test_seekable_stream.py b/fastapi/tests/test_seekable_stream.py deleted file mode 100644 index 12001b36e..000000000 --- a/fastapi/tests/test_seekable_stream.py +++ /dev/null @@ -1,86 +0,0 @@ -import io -import random - -from odoo.tests.common import TransactionCase - -from ..seekable_stream import SeekableStream - - -class TestSeekableStream(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # create a random large content - cls.original_content = random.randbytes(1024 * 1024) - - def setUp(self): - super().setUp() - self.original_stream = NonSeekableStream(self.original_content) - - def test_read_all(self): - self.assertFalse(self.original_stream.seekable()) - stream = SeekableStream(self.original_stream) - data = stream.read() - self.assertEqual(data, self.original_content) - stream.seek(0) - data = stream.read() - self.assertEqual(data, self.original_content) - - def test_read_partial(self): - self.assertFalse(self.original_stream.seekable()) - stream = SeekableStream(self.original_stream) - data = stream.read(10) - self.assertEqual(data, self.original_content[:10]) - data = stream.read(10) - self.assertEqual(data, self.original_content[10:20]) - # read the rest - data = stream.read() - self.assertEqual(data, self.original_content[20:]) - - def test_seek(self): - self.assertFalse(self.original_stream.seekable()) - stream = SeekableStream(self.original_stream) - stream.seek(10) - self.assertEqual(stream.tell(), 10) - data = stream.read(10) - self.assertEqual(data, self.original_content[10:20]) - stream.seek(0) - self.assertEqual(stream.tell(), 0) - data = stream.read(10) - self.assertEqual(data, self.original_content[:10]) - - def test_seek_relative(self): - self.assertFalse(self.original_stream.seekable()) - stream = SeekableStream(self.original_stream) - stream.seek(10) - self.assertEqual(stream.tell(), 10) - stream.seek(5, io.SEEK_CUR) - self.assertEqual(stream.tell(), 15) - data = stream.read(10) - self.assertEqual(data, self.original_content[15:25]) - - def test_seek_end(self): - self.assertFalse(self.original_stream.seekable()) - stream = SeekableStream(self.original_stream) - stream.seek(-10, io.SEEK_END) - self.assertEqual(stream.tell(), len(self.original_content) - 10) - data = stream.read(10) - self.assertEqual(data, self.original_content[-10:]) - stream.seek(0, io.SEEK_END) - self.assertEqual(stream.tell(), len(self.original_content)) - data = stream.read(10) - self.assertEqual(data, b"") - stream.seek(-len(self.original_content), io.SEEK_END) - self.assertEqual(stream.tell(), 0) - data = stream.read(10) - - -class NonSeekableStream(io.BytesIO): - def seekable(self): - return False - - def seek(self, offset, whence=io.SEEK_SET): - raise io.UnsupportedOperation("seek") - - def tell(self): - raise io.UnsupportedOperation("tell") From 60aeb5f80e423e887c47e2ab3c644a19dc5a5b50 Mon Sep 17 00:00:00 2001 From: Zina Rasoamanana Date: Thu, 8 Aug 2024 14:50:41 +0200 Subject: [PATCH 4/7] [16.0][FIX] fastapi: paging - correctly set authorised minimal value in paging function --- fastapi/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies.py b/fastapi/dependencies.py index 4835c74c9..47b133285 100644 --- a/fastapi/dependencies.py +++ b/fastapi/dependencies.py @@ -101,7 +101,7 @@ def optionally_authenticated_partner( def paging( - page: Annotated[int, Query(gte=1)] = 1, page_size: Annotated[int, Query(gte=1)] = 80 + page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1)] = 80 ) -> Paging: """Return a Paging object from the page and page_size parameters""" return Paging(limit=page_size, offset=(page - 1) * page_size) From d6fbe4740386e2f3f9d2c877083475ffded6dc75 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 9 Jul 2024 12:05:57 +0200 Subject: [PATCH 5/7] [FIX] fastapi: Unflag save_session in routing info --- fastapi/models/fastapi_endpoint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index de465a9a7..4a787a864 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -178,6 +178,7 @@ def _get_routing_info(self): f"{self.root_path}/", f"{self.root_path}/", ], + "save_session": False, # csrf ????? } From 6607300a9bb6711732cc058899583694f1e6f375 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 1 Oct 2024 11:19:06 +0200 Subject: [PATCH 6/7] [IMP] fastapi: Makes the save_session configurable --- fastapi/models/fastapi_endpoint.py | 12 +++++++++++- fastapi/readme/newsfragments/442.feature | 1 + fastapi/views/fastapi_endpoint.xml | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 fastapi/readme/newsfragments/442.feature diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 4a787a864..e7ccf7674 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -55,6 +55,16 @@ class FastapiEndpoint(models.Model): readonly=False, domain="[('user_ids', 'in', user_id)]", ) + save_http_session = fields.Boolean( + string="Save HTTP Session", + help="Whether session should be saved into the session store. This is " + "required if for example you use the Odoo's authentication mechanism. " + "Oherwise chance are high that you don't need it and could turn off " + "this behaviour. Additionaly turning off this option will prevent useless " + "IO operation when storing and reading the session on the disk and prevent " + "unexpecteed disk space consumption.", + default=True, + ) @api.depends("root_path") def _compute_root_path(self): @@ -178,7 +188,7 @@ def _get_routing_info(self): f"{self.root_path}/", f"{self.root_path}/", ], - "save_session": False, + "save_session": self.save_http_session, # csrf ????? } diff --git a/fastapi/readme/newsfragments/442.feature b/fastapi/readme/newsfragments/442.feature new file mode 100644 index 000000000..f45f73ebf --- /dev/null +++ b/fastapi/readme/newsfragments/442.feature @@ -0,0 +1 @@ +* A new parameter is now available on the endpoint model to let you disable the creation and the store of session files used by Odoo for calls to your application endpoint. This is usefull to prevent disk space consumption and IO operations if your application doesn't need to use this sessions files which are mainly used by Odoo by to store the session info of logged in users. diff --git a/fastapi/views/fastapi_endpoint.xml b/fastapi/views/fastapi_endpoint.xml index 5b0d0a46d..77566647d 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -46,7 +46,7 @@ - + From c056a160f0694b49a00125a080f3106065760c1e Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 1 Oct 2024 11:38:51 +0200 Subject: [PATCH 7/7] [LINT] fastapi --- fastapi/views/fastapi_endpoint.xml | 3 ++- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fastapi/views/fastapi_endpoint.xml b/fastapi/views/fastapi_endpoint.xml index 77566647d..1f1516be3 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -46,7 +46,8 @@ - + + diff --git a/requirements.txt b/requirements.txt index abc1ab723..bcba9b734 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # generated from manifests external_dependencies -a2wsgi -fastapi +a2wsgi>=1.10.6 +fastapi>=0.110.0 parse-accept-language python-multipart ujson