From f38a6d18888ae60d7b1eb80906e62eb1ae0bd625 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 13 Nov 2023 18:12:03 +0100 Subject: [PATCH 01/12] [FIX] fastapi: In test class don't override overrides If you pass an override of the authenticated_partner_impl to the _create_test_client method, don't override it by a default one --- fastapi/README.rst | 2 +- fastapi/readme/newsfragments/396.bugfix | 9 +++++++++ fastapi/static/description/index.html | 2 +- fastapi/tests/common.py | 8 +++++++- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 fastapi/readme/newsfragments/396.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index 9a340340..88bcecad 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:7e852b9c3fc9194cdb6aec159a4d1b139ba10dc15b9aecd5158e0ffe1308a860 + !! source digest: sha256:c1bd4722eaa873dcff3df33f91ca316004453aea2b43c9184928767f267edaaa !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/readme/newsfragments/396.bugfix b/fastapi/readme/newsfragments/396.bugfix new file mode 100644 index 00000000..cd71fcea --- /dev/null +++ b/fastapi/readme/newsfragments/396.bugfix @@ -0,0 +1,9 @@ +When using the 'FastAPITransactionCase' class, allows to specify a specific +override of the 'authenticated_partner_impl' method into the list of +overrides to apply. Before this change, the 'authenticated_partner_impl' +override given in the 'overrides' parameter was always overridden in the +'_create_test_client' method of the 'FastAPITransactionCase' class. It's now +only overridden if the 'authenticated_partner_impl' method is not already +present in the list of overrides to apply and no specific partner is given. +If a specific partner is given at same time of an override for the +'authenticated_partner_impl' method, an error is raised. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 02859660..f704a0c7 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -367,7 +367,7 @@

Odoo FastAPI

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:7e852b9c3fc9194cdb6aec159a4d1b139ba10dc15b9aecd5158e0ffe1308a860 +!! source digest: sha256:c1bd4722eaa873dcff3df33f91ca316004453aea2b43c9184928767f267edaaa !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This addon provides the basis to smoothly integrate the FastAPI diff --git a/fastapi/tests/common.py b/fastapi/tests/common.py index 80e70cfc..11bcc1b5 100644 --- a/fastapi/tests/common.py +++ b/fastapi/tests/common.py @@ -98,7 +98,13 @@ def _create_test_client( or self.default_fastapi_authenticated_partner or self.env["res.partner"] ) - dependencies[authenticated_partner_impl] = partial(lambda a: a, partner) + if partner and authenticated_partner_impl in dependencies: + raise ValueError( + "You cannot provide an override for the authenticated_partner_impl " + "dependency when creating a test client with a partner." + ) + if partner or authenticated_partner_impl not in dependencies: + dependencies[authenticated_partner_impl] = partial(lambda a: a, partner) app = app or self.default_fastapi_app or FastAPI() router = router or self.default_fastapi_router if router: From 47282647ba9fae7ab68254c632105336867b8341 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 12 Dec 2023 08:49:03 +0000 Subject: [PATCH 02/12] [BOT] post-merge updates --- fastapi/README.rst | 18 ++++++++- fastapi/__manifest__.py | 2 +- fastapi/readme/HISTORY.rst | 16 ++++++++ fastapi/readme/newsfragments/396.bugfix | 9 ----- fastapi/static/description/index.html | 49 ++++++++++++++++--------- 5 files changed, 66 insertions(+), 28 deletions(-) delete mode 100644 fastapi/readme/newsfragments/396.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index 88bcecad..b020e393 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:c1bd4722eaa873dcff3df33f91ca316004453aea2b43c9184928767f267edaaa + !! source digest: sha256:d3ecfeda1c0c52b03f8f326849edbfac58a4c724c6e4e2d90e7c5edd6f5207c8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -1468,6 +1468,22 @@ WebSockets and to stream large responses. Changelog ========= +16.0.1.2.2 (2023-12-12) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- When using the 'FastAPITransactionCase' class, allows to specify a specific + override of the 'authenticated_partner_impl' method into the list of + overrides to apply. Before this change, the 'authenticated_partner_impl' + override given in the 'overrides' parameter was always overridden in the + '_create_test_client' method of the 'FastAPITransactionCase' class. It's now + only overridden if the 'authenticated_partner_impl' method is not already + present in the list of overrides to apply and no specific partner is given. + If a specific partner is given at same time of an override for the + 'authenticated_partner_impl' method, an error is raised. (`#396 `_) + + 16.0.1.2.1 (2023-11-03) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 2c4ef1de..43324c24 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.2.1", + "version": "16.0.1.2.2", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/readme/HISTORY.rst b/fastapi/readme/HISTORY.rst index c7d17e7d..53c58945 100644 --- a/fastapi/readme/HISTORY.rst +++ b/fastapi/readme/HISTORY.rst @@ -1,3 +1,19 @@ +16.0.1.2.2 (2023-12-12) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- When using the 'FastAPITransactionCase' class, allows to specify a specific + override of the 'authenticated_partner_impl' method into the list of + overrides to apply. Before this change, the 'authenticated_partner_impl' + override given in the 'overrides' parameter was always overridden in the + '_create_test_client' method of the 'FastAPITransactionCase' class. It's now + only overridden if the 'authenticated_partner_impl' method is not already + present in the list of overrides to apply and no specific partner is given. + If a specific partner is given at same time of an override for the + 'authenticated_partner_impl' method, an error is raised. (`#396 `_) + + 16.0.1.2.1 (2023-11-03) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fastapi/readme/newsfragments/396.bugfix b/fastapi/readme/newsfragments/396.bugfix deleted file mode 100644 index cd71fcea..00000000 --- a/fastapi/readme/newsfragments/396.bugfix +++ /dev/null @@ -1,9 +0,0 @@ -When using the 'FastAPITransactionCase' class, allows to specify a specific -override of the 'authenticated_partner_impl' method into the list of -overrides to apply. Before this change, the 'authenticated_partner_impl' -override given in the 'overrides' parameter was always overridden in the -'_create_test_client' method of the 'FastAPITransactionCase' class. It's now -only overridden if the 'authenticated_partner_impl' method is not already -present in the list of overrides to apply and no specific partner is given. -If a specific partner is given at same time of an override for the -'authenticated_partner_impl' method, an error is raised. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index f704a0c7..2c0fdcc2 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,7 +366,7 @@

Odoo FastAPI

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:c1bd4722eaa873dcff3df33f91ca316004453aea2b43c9184928767f267edaaa +!! source digest: sha256:d3ecfeda1c0c52b03f8f326849edbfac58a4c724c6e4e2d90e7c5edd6f5207c8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This addon provides the basis to smoothly integrate the FastAPI @@ -432,15 +431,16 @@

Odoo FastAPI

  • Known issues / Roadmap
  • Changelog
  • -
  • Bug Tracker
  • -
  • Credits @@ -1700,7 +1700,22 @@

    Known issues / Roadmap

    Changelog

    -

    16.0.1.2.1 (2023-11-03)

    +

    16.0.1.2.2 (2023-12-12)

    +

    Bugfixes

    +
      +
    • When using the ‘FastAPITransactionCase’ class, allows to specify a specific +override of the ‘authenticated_partner_impl’ method into the list of +overrides to apply. Before this change, the ‘authenticated_partner_impl’ +override given in the ‘overrides’ parameter was always overridden in the +‘_create_test_client’ method of the ‘FastAPITransactionCase’ class. It’s now +only overridden if the ‘authenticated_partner_impl’ method is not already +present in the list of overrides to apply and no specific partner is given. +If a specific partner is given at same time of an override for the +‘authenticated_partner_impl’ method, an error is raised. (#396)
    • +
    +
    +
    +

    16.0.1.2.1 (2023-11-03)

    Bugfixes

    • Fix a typo in the Field declaration of the ‘count’ attribute of the ‘PagedCollection’ schema.

      @@ -1708,8 +1723,8 @@

      16.0.1.2.1 (2023-11-03)

    -
    -

    16.0.1.2.0 (2023-10-13)

    +
    +

    16.0.1.2.0 (2023-10-13)

    Features

    • The field total in the PagedCollection schema is replaced by the field count. @@ -1723,7 +1738,7 @@

      16.0.1.2.0 (2023-10-13)

    -

    Bug Tracker

    +

    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 @@ -1731,21 +1746,21 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association

    OCA, or the Odoo Community Association, is a nonprofit organization whose From 663e51fb66c2c1808a512f72e91997302aa277f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 18 Dec 2023 15:54:34 +0100 Subject: [PATCH 03/12] [FIX] fastapi: close cursor after rollback This is to ensure that `retrying` in `service/model.py` does not try to flush. --- fastapi/README.rst | 2 +- fastapi/error_handlers.py | 7 ++++++- fastapi/readme/newsfragments/405.bugfix | 4 ++++ fastapi/static/description/index.html | 3 ++- fastapi/tests/test_fastapi_demo.py | 5 ++++- 5 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 fastapi/readme/newsfragments/405.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index b020e393..ef483e82 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d3ecfeda1c0c52b03f8f326849edbfac58a4c724c6e4e2d90e7c5edd6f5207c8 + !! source digest: sha256:3c6fc2f30a2e71ad5f97cd59022ea69c1c7a812bc791555ace9206f7c18c82ae !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/error_handlers.py b/fastapi/error_handlers.py index a7b4d9b2..2b3584b6 100644 --- a/fastapi/error_handlers.py +++ b/fastapi/error_handlers.py @@ -23,10 +23,15 @@ def _rollback(request: Request, reason: str) -> None: - cr = odoo_env_ctx.get().cr + env = odoo_env_ctx.get() + cr = env.cr if cr is not None: _logger.debug("rollback on %s", reason) cr.rollback() + # Also close the cursor, so `retrying` in service/model.py does not attempt to + # flush. + if not (env.registry.in_test_mode()): + cr.close() async def _odoo_user_error_handler( diff --git a/fastapi/readme/newsfragments/405.bugfix b/fastapi/readme/newsfragments/405.bugfix new file mode 100644 index 00000000..3d02d8d9 --- /dev/null +++ b/fastapi/readme/newsfragments/405.bugfix @@ -0,0 +1,4 @@ +In case of exception in endpoint execution, close the database cursor after rollback. + +This is to ensure that the *retrying* method in *service/model.py* does not try +to flush data to the database. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 2c0fdcc2..bf0556e7 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,3 +1,4 @@ + @@ -366,7 +367,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d3ecfeda1c0c52b03f8f326849edbfac58a4c724c6e4e2d90e7c5edd6f5207c8 +!! source digest: sha256:3c6fc2f30a2e71ad5f97cd59022ea69c1c7a812bc791555ace9206f7c18c82ae !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI diff --git a/fastapi/tests/test_fastapi_demo.py b/fastapi/tests/test_fastapi_demo.py index 5cd9fef5..248a39fd 100644 --- a/fastapi/tests/test_fastapi_demo.py +++ b/fastapi/tests/test_fastapi_demo.py @@ -47,7 +47,9 @@ def assert_exception_processed( demo_app._get_app(), raise_server_exceptions=False ) as test_client, mock.patch.object( self.env.cr.__class__, "rollback" - ) as mock_rollback: + ) as mock_rollback, mock.patch.object( + self.env.cr.__class__, "close" + ) as mock_close: response: Response = test_client.get( "/demo/exception", params={ @@ -56,6 +58,7 @@ def assert_exception_processed( }, ) mock_rollback.assert_called_once() + mock_close.assert_called_once() self.assertEqual(response.status_code, expected_status_code) self.assertDictEqual( response.json(), From 603a49c5be5c907460ccccee819a99c44d5ec617 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 21 Dec 2023 11:10:26 +0000 Subject: [PATCH 04/12] [BOT] post-merge updates --- fastapi/README.rst | 13 ++++++- fastapi/__manifest__.py | 2 +- fastapi/readme/HISTORY.rst | 11 ++++++ fastapi/readme/newsfragments/405.bugfix | 4 -- fastapi/static/description/index.html | 50 +++++++++++++++---------- 5 files changed, 54 insertions(+), 26 deletions(-) delete mode 100644 fastapi/readme/newsfragments/405.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index ef483e82..fe3a857c 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3c6fc2f30a2e71ad5f97cd59022ea69c1c7a812bc791555ace9206f7c18c82ae + !! source digest: sha256:b17a5ca250ac0d53b7e0a5952c348ca05d5e61da98bbc767904730e59de3eaab !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -1468,6 +1468,17 @@ WebSockets and to stream large responses. Changelog ========= +16.0.1.2.3 (2023-12-21) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- In case of exception in endpoint execution, close the database cursor after rollback. + + This is to ensure that the *retrying* method in *service/model.py* does not try + to flush data to the database. (`#405 `_) + + 16.0.1.2.2 (2023-12-12) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 43324c24..a73fd157 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.2.2", + "version": "16.0.1.2.3", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/readme/HISTORY.rst b/fastapi/readme/HISTORY.rst index 53c58945..dd3b7970 100644 --- a/fastapi/readme/HISTORY.rst +++ b/fastapi/readme/HISTORY.rst @@ -1,3 +1,14 @@ +16.0.1.2.3 (2023-12-21) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- In case of exception in endpoint execution, close the database cursor after rollback. + + This is to ensure that the *retrying* method in *service/model.py* does not try + to flush data to the database. (`#405 `_) + + 16.0.1.2.2 (2023-12-12) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fastapi/readme/newsfragments/405.bugfix b/fastapi/readme/newsfragments/405.bugfix deleted file mode 100644 index 3d02d8d9..00000000 --- a/fastapi/readme/newsfragments/405.bugfix +++ /dev/null @@ -1,4 +0,0 @@ -In case of exception in endpoint execution, close the database cursor after rollback. - -This is to ensure that the *retrying* method in *service/model.py* does not try -to flush data to the database. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index bf0556e7..ecea0dea 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,7 +366,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:3c6fc2f30a2e71ad5f97cd59022ea69c1c7a812bc791555ace9206f7c18c82ae +!! source digest: sha256:b17a5ca250ac0d53b7e0a5952c348ca05d5e61da98bbc767904730e59de3eaab !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI @@ -432,16 +431,17 @@

    Odoo FastAPI

  • Known issues / Roadmap
  • Changelog
  • -
  • Bug Tracker
  • -
  • Credits @@ -1701,7 +1701,17 @@

    Known issues / Roadmap

    Changelog

    -

    16.0.1.2.2 (2023-12-12)

    +

    16.0.1.2.3 (2023-12-21)

    +

    Bugfixes

    +
      +
    • In case of exception in endpoint execution, close the database cursor after rollback.

      +

      This is to ensure that the retrying method in service/model.py does not try +to flush data to the database. (#405)

      +
    • +
    +
    +
    +

    16.0.1.2.2 (2023-12-12)

    Bugfixes

    • When using the ‘FastAPITransactionCase’ class, allows to specify a specific @@ -1715,8 +1725,8 @@

      16.0.1.2.2 (2023-12-12)

      ‘authenticated_partner_impl’ method, an error is raised. (#396)
    -
    -

    16.0.1.2.1 (2023-11-03)

    +
    +

    16.0.1.2.1 (2023-11-03)

    Bugfixes

    • Fix a typo in the Field declaration of the ‘count’ attribute of the ‘PagedCollection’ schema.

      @@ -1724,8 +1734,8 @@

      16.0.1.2.1 (2023-11-03)

    -
    -

    16.0.1.2.0 (2023-10-13)

    +
    +

    16.0.1.2.0 (2023-10-13)

    Features

    • The field total in the PagedCollection schema is replaced by the field count. @@ -1739,7 +1749,7 @@

      16.0.1.2.0 (2023-10-13)

    -

    Bug Tracker

    +

    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 @@ -1747,21 +1757,21 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association

    OCA, or the Odoo Community Association, is a nonprofit organization whose From c8d4fd50c0810cfc1eb2fe8180cf97d483a27120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 21 Dec 2023 16:33:47 +0100 Subject: [PATCH 05/12] Revert "[FIX] fastapi: close cursor after rollback" This reverts commit 8be7fbb0fda92b46e4913176185fb4e19ab319be. --- fastapi/error_handlers.py | 7 +------ fastapi/tests/test_fastapi_demo.py | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/fastapi/error_handlers.py b/fastapi/error_handlers.py index 2b3584b6..a7b4d9b2 100644 --- a/fastapi/error_handlers.py +++ b/fastapi/error_handlers.py @@ -23,15 +23,10 @@ def _rollback(request: Request, reason: str) -> None: - env = odoo_env_ctx.get() - cr = env.cr + cr = odoo_env_ctx.get().cr if cr is not None: _logger.debug("rollback on %s", reason) cr.rollback() - # Also close the cursor, so `retrying` in service/model.py does not attempt to - # flush. - if not (env.registry.in_test_mode()): - cr.close() async def _odoo_user_error_handler( diff --git a/fastapi/tests/test_fastapi_demo.py b/fastapi/tests/test_fastapi_demo.py index 248a39fd..5cd9fef5 100644 --- a/fastapi/tests/test_fastapi_demo.py +++ b/fastapi/tests/test_fastapi_demo.py @@ -47,9 +47,7 @@ def assert_exception_processed( demo_app._get_app(), raise_server_exceptions=False ) as test_client, mock.patch.object( self.env.cr.__class__, "rollback" - ) as mock_rollback, mock.patch.object( - self.env.cr.__class__, "close" - ) as mock_close: + ) as mock_rollback: response: Response = test_client.get( "/demo/exception", params={ @@ -58,7 +56,6 @@ def assert_exception_processed( }, ) mock_rollback.assert_called_once() - mock_close.assert_called_once() self.assertEqual(response.status_code, expected_status_code) self.assertDictEqual( response.json(), From d6ed7b9257454830e4a03e53bf2a56d0fc183feb Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 21 Dec 2023 15:38:44 +0000 Subject: [PATCH 06/12] [BOT] post-merge updates --- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/README.rst b/fastapi/README.rst index fe3a857c..44a1d81b 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b17a5ca250ac0d53b7e0a5952c348ca05d5e61da98bbc767904730e59de3eaab + !! source digest: sha256:2b79ba7bbc412d35210ba755b9fca17352faad894aa88c9339fb7063d02a50bf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index a73fd157..479377ba 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.2.3", + "version": "16.0.1.2.4", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index ecea0dea..1a75628e 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -366,7 +366,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:b17a5ca250ac0d53b7e0a5952c348ca05d5e61da98bbc767904730e59de3eaab +!! source digest: sha256:2b79ba7bbc412d35210ba755b9fca17352faad894aa88c9339fb7063d02a50bf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From d50ec2465f4f49ce37cb2be8cdba5efdce17d3cb Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 10 Jan 2024 08:12:08 +0000 Subject: [PATCH 07/12] Added translation using Weblate (Italian) --- fastapi/i18n/it.po | 236 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 fastapi/i18n/it.po diff --git a/fastapi/i18n/it.po b/fastapi/i18n/it.po new file mode 100644 index 00000000..549401c4 --- /dev/null +++ b/fastapi/i18n/it.po @@ -0,0 +1,236 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: fastapi +#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__description +msgid "A short description of the API. It can use Markdown" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__active +msgid "Active" +msgstr "" + +#. module: fastapi +#: model:res.groups,name:fastapi.group_fastapi_manager +msgid "Administrator" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__api_key +msgid "Api Key" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__app +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view +msgid "App" +msgstr "" + +#. module: fastapi +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view +msgid "Archived" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method +msgid "Authenciation method" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id +msgid "Company" +msgstr "" + +#. module: fastapi +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_demo_form_view +msgid "Configuration" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_uid +msgid "Created by" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_date +msgid "Created on" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__app__demo +msgid "Demo Endpoint" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__description +msgid "Description" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__display_name +msgid "Display Name" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__docs_url +msgid "Docs Url" +msgstr "" + +#. module: fastapi +#: model:ir.module.category,name:fastapi.module_category_fastapi +#: model:ir.ui.menu,name:fastapi.menu_fastapi_root +msgid "FastAPI" +msgstr "" + +#. module: fastapi +#: model:ir.actions.act_window,name:fastapi.fastapi_endpoint_act_window +#: model:ir.model,name:fastapi.model_fastapi_endpoint +#: model:ir.ui.menu,name:fastapi.fastapi_endpoint_menu +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi +#: model:res.groups,name:fastapi.group_fastapi_endpoint_runner +msgid "FastAPI Endpoint Runner" +msgstr "" + +#. module: fastapi +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view +msgid "Group by..." +msgstr "" + +#. module: fastapi +#: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__http_basic +msgid "HTTP Basic" +msgstr "" + +#. module: fastapi +#: model:ir.module.category,description:fastapi.module_category_fastapi +msgid "Helps you manage your Fastapi Endpoints" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__id +msgid "ID" +msgstr "" + +#. module: fastapi +#: model:ir.model,name:fastapi.model_res_lang +msgid "Languages" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fastapi +#: model:res.groups,name:fastapi.my_demo_app_group +msgid "My Demo Endpoint Group" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__name +msgid "Name" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__registry_sync +msgid "" +"ON: the record has been modified and registry was not notified.\n" +"No change will be active until this flag is set to false via proper action.\n" +"\n" +"OFF: record in line with the registry, nothing to do." +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__openapi_url +msgid "Openapi Url" +msgstr "" + +#. module: fastapi +#: model:ir.model,name:fastapi.model_ir_rule +msgid "Record Rule" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__redoc_url +msgid "Redoc Url" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__registry_sync +msgid "Registry Sync" +msgstr "" + +#. module: fastapi +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view +msgid "Registry Sync Required" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__root_path +msgid "Root Path" +msgstr "" + +#. module: fastapi +#: model:ir.actions.server,name:fastapi.fastapi_endpoint_action_sync_registry +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view +#: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_tree_view +msgid "Sync Registry" +msgstr "" + +#. module: fastapi +#. odoo-python +#: code:addons/fastapi/models/fastapi_endpoint_demo.py:0 +#, python-format +msgid "The authentication method is required for app %(app)s" +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__name +msgid "The title of the API." +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__user_id +msgid "The user to use to execute the API calls." +msgstr "" + +#. module: fastapi +#: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__user_id +#: model:res.groups,name:fastapi.group_fastapi_user +msgid "User" +msgstr "" + +#. module: fastapi +#. odoo-python +#: code:addons/fastapi/models/fastapi_endpoint.py:0 +#, python-format +msgid "`%(name)s` uses a blacklisted root_path = `%(root_path)s`" +msgstr "" From 68c610241fc5d1a4f876ca4424643764a700a4b4 Mon Sep 17 00:00:00 2001 From: mymage Date: Mon, 15 Jan 2024 15:02:53 +0000 Subject: [PATCH 08/12] Translated using Weblate (Italian) Currently translated at 100.0% (41 of 41 strings) Translation: rest-framework-16.0/rest-framework-16.0-fastapi Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi/it/ --- fastapi/i18n/it.po | 89 +++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/fastapi/i18n/it.po b/fastapi/i18n/it.po index 549401c4..74ddd5ec 100644 --- a/fastapi/i18n/it.po +++ b/fastapi/i18n/it.po @@ -6,158 +6,160 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2024-01-15 17:34+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" #. module: fastapi #: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__description msgid "A short description of the API. It can use Markdown" -msgstr "" +msgstr "Una breve descrizione dell'API. Può contenere Markdown" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__active msgid "Active" -msgstr "" +msgstr "Attivo" #. module: fastapi #: model:res.groups,name:fastapi.group_fastapi_manager msgid "Administrator" -msgstr "" +msgstr "Amministratore" #. module: fastapi #: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__api_key msgid "Api Key" -msgstr "" +msgstr "Chiave API" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__app #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view msgid "App" -msgstr "" +msgstr "Applicazione" #. module: fastapi #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view msgid "Archived" -msgstr "" +msgstr "In archivio" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method msgid "Authenciation method" -msgstr "" +msgstr "Metodo autenticazione" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id msgid "Company" -msgstr "" +msgstr "Azienda" #. module: fastapi #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_demo_form_view msgid "Configuration" -msgstr "" +msgstr "Configurazione" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: fastapi #: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__app__demo msgid "Demo Endpoint" -msgstr "" +msgstr "Endpoint esempio" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__description msgid "Description" -msgstr "" +msgstr "Descrizione" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__docs_url msgid "Docs Url" -msgstr "" +msgstr "URL documenti" #. module: fastapi #: model:ir.module.category,name:fastapi.module_category_fastapi #: model:ir.ui.menu,name:fastapi.menu_fastapi_root msgid "FastAPI" -msgstr "" +msgstr "FastAPI" #. module: fastapi #: model:ir.actions.act_window,name:fastapi.fastapi_endpoint_act_window #: model:ir.model,name:fastapi.model_fastapi_endpoint #: model:ir.ui.menu,name:fastapi.fastapi_endpoint_menu msgid "FastAPI Endpoint" -msgstr "" +msgstr "Endopoint FastAPI" #. module: fastapi #: model:res.groups,name:fastapi.group_fastapi_endpoint_runner msgid "FastAPI Endpoint Runner" -msgstr "" +msgstr "Esecutore endopoint FastAPI" #. module: fastapi #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_search_view msgid "Group by..." -msgstr "" +msgstr "Raggruppa per..." #. module: fastapi #: model:ir.model.fields.selection,name:fastapi.selection__fastapi_endpoint__demo_auth_method__http_basic msgid "HTTP Basic" -msgstr "" +msgstr "Base HTTP" #. module: fastapi #: model:ir.module.category,description:fastapi.module_category_fastapi msgid "Helps you manage your Fastapi Endpoints" -msgstr "" +msgstr "Aiuta nella gestione degli endpoint FastAPI" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__id msgid "ID" -msgstr "" +msgstr "ID" #. module: fastapi #: model:ir.model,name:fastapi.model_res_lang msgid "Languages" -msgstr "" +msgstr "Lingue" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento di" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: fastapi #: model:res.groups,name:fastapi.my_demo_app_group msgid "My Demo Endpoint Group" -msgstr "" +msgstr "Il mio gruppo endpoint esempio" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__name msgid "Name" -msgstr "" +msgstr "Nome" #. module: fastapi #: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__registry_sync @@ -167,70 +169,75 @@ msgid "" "\n" "OFF: record in line with the registry, nothing to do." msgstr "" +"Acceso: il record è stato modificato e il registro non è stato notificato.\n" +"Nessuna modifica sarà attiva finchè questa opzione è impostata a falso " +"attraverso un'azione opportuna.\n" +"\n" +"Spento: record allineato con il registro, non c'è niente da fare." #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__openapi_url msgid "Openapi Url" -msgstr "" +msgstr "URL OpenAPI" #. module: fastapi #: model:ir.model,name:fastapi.model_ir_rule msgid "Record Rule" -msgstr "" +msgstr "Regola su record" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__redoc_url msgid "Redoc Url" -msgstr "" +msgstr "URL record" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__registry_sync msgid "Registry Sync" -msgstr "" +msgstr "Sincro registro" #. module: fastapi #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view msgid "Registry Sync Required" -msgstr "" +msgstr "Sincro registro richiesto" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__root_path msgid "Root Path" -msgstr "" +msgstr "Percorso radice" #. module: fastapi #: model:ir.actions.server,name:fastapi.fastapi_endpoint_action_sync_registry #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_form_view #: model_terms:ir.ui.view,arch_db:fastapi.fastapi_endpoint_tree_view msgid "Sync Registry" -msgstr "" +msgstr "Sincronizza registro" #. module: fastapi #. odoo-python #: code:addons/fastapi/models/fastapi_endpoint_demo.py:0 #, python-format msgid "The authentication method is required for app %(app)s" -msgstr "" +msgstr "Il metodo di autenticazione è richiesto per l'app %(app)s" #. module: fastapi #: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__name msgid "The title of the API." -msgstr "" +msgstr "Titolo dell'API." #. module: fastapi #: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__user_id msgid "The user to use to execute the API calls." -msgstr "" +msgstr "Utente da utilizzare per eseguire la chiamata API." #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__user_id #: model:res.groups,name:fastapi.group_fastapi_user msgid "User" -msgstr "" +msgstr "Utente" #. module: fastapi #. odoo-python #: code:addons/fastapi/models/fastapi_endpoint.py:0 #, python-format msgid "`%(name)s` uses a blacklisted root_path = `%(root_path)s`" -msgstr "" +msgstr "`%(name)s` utilizza un root_path bloccato = `%(root_path)s`" From 2c2015604c7348bc7e96bae6b740a76a700045aa Mon Sep 17 00:00:00 2001 From: Zina Rasoamanana Date: Fri, 12 Jan 2024 15:34:25 +0100 Subject: [PATCH 09/12] [16.0][FIX] fastapi: always add authenticate_partner_id in context when evaluating ir rules This fix is needed since a modificiation of ir.rule that checks the domain when creating/modifying ir rules The solution is to set authenticate_partner_id to False when it is not present in context --- fastapi/README.rst | 2 +- fastapi/fastapi_dispatcher.py | 4 ---- fastapi/models/ir_rule.py | 7 +++---- fastapi/readme/newsfragments/410.bugfix | 5 +++++ fastapi/static/description/index.html | 3 ++- 5 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 fastapi/readme/newsfragments/410.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index 44a1d81b..37236f98 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:2b79ba7bbc412d35210ba755b9fca17352faad894aa88c9339fb7063d02a50bf + !! source digest: sha256:8a90dec844eac96b44cefb64548fb05283ade12b16246bb89062abc7644e2087 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index a78e3f81..904deeff 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -49,10 +49,6 @@ def _get_environ(self): @contextmanager def _manage_odoo_env(self, uid=None): env = request.env - # add authenticated_partner_id=False in the context - # to ensure that the ir.rule defined for user's endpoint can be - # evaluated even if not authenticated partner is set - env = env(context=dict(env.context, authenticated_partner_id=False)) accept_language = request.httprequest.headers.get("Accept-language") context = env.context if accept_language: diff --git a/fastapi/models/ir_rule.py b/fastapi/models/ir_rule.py index b1d10fc5..68bda16f 100644 --- a/fastapi/models/ir_rule.py +++ b/fastapi/models/ir_rule.py @@ -17,10 +17,9 @@ class IrRule(models.Model): @api.model def _eval_context(self): ctx = super()._eval_context() - if "authenticated_partner_id" in self.env.context: - ctx["authenticated_partner_id"] = self.env.context[ - "authenticated_partner_id" - ] + ctx["authenticated_partner_id"] = self.env.context.get( + "authenticated_partner_id", False + ) return ctx def _compute_domain_keys(self): diff --git a/fastapi/readme/newsfragments/410.bugfix b/fastapi/readme/newsfragments/410.bugfix new file mode 100644 index 00000000..23ad39e2 --- /dev/null +++ b/fastapi/readme/newsfragments/410.bugfix @@ -0,0 +1,5 @@ +Odoo has done an update and now, it checks domains of ir.rule on creation and modification. + +The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context. +This field wasn't always set and this caused an error when Odoo checked the domain. +So now it is set to *False* by default. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 1a75628e..a1c84d0a 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,3 +1,4 @@ + @@ -366,7 +367,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:2b79ba7bbc412d35210ba755b9fca17352faad894aa88c9339fb7063d02a50bf +!! source digest: sha256:8a90dec844eac96b44cefb64548fb05283ade12b16246bb89062abc7644e2087 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From aa3b98f288ec846c7b1c3b396bdb17171ae740e6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 17 Jan 2024 16:10:33 +0000 Subject: [PATCH 10/12] [BOT] post-merge updates --- fastapi/README.rst | 14 +++++- fastapi/__manifest__.py | 2 +- fastapi/readme/HISTORY.rst | 12 ++++++ fastapi/readme/newsfragments/410.bugfix | 5 --- fastapi/static/description/index.html | 57 +++++++++++++++---------- 5 files changed, 60 insertions(+), 30 deletions(-) delete mode 100644 fastapi/readme/newsfragments/410.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index 37236f98..438bf8cd 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:8a90dec844eac96b44cefb64548fb05283ade12b16246bb89062abc7644e2087 + !! source digest: sha256:439e51bdd708113a0ee8a644f2ca42d29c286af1aee25fa6035b743321969c88 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -1468,6 +1468,18 @@ WebSockets and to stream large responses. Changelog ========= +16.0.1.2.5 (2024-01-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Odoo has done an update and now, it checks domains of ir.rule on creation and modification. + + The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context. + This field wasn't always set and this caused an error when Odoo checked the domain. + So now it is set to *False* by default. (`#410 `_) + + 16.0.1.2.3 (2023-12-21) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 479377ba..57ba1144 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.2.4", + "version": "16.0.1.2.5", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/readme/HISTORY.rst b/fastapi/readme/HISTORY.rst index dd3b7970..54d0c7f1 100644 --- a/fastapi/readme/HISTORY.rst +++ b/fastapi/readme/HISTORY.rst @@ -1,3 +1,15 @@ +16.0.1.2.5 (2024-01-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Odoo has done an update and now, it checks domains of ir.rule on creation and modification. + + The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context. + This field wasn't always set and this caused an error when Odoo checked the domain. + So now it is set to *False* by default. (`#410 `_) + + 16.0.1.2.3 (2023-12-21) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/fastapi/readme/newsfragments/410.bugfix b/fastapi/readme/newsfragments/410.bugfix deleted file mode 100644 index 23ad39e2..00000000 --- a/fastapi/readme/newsfragments/410.bugfix +++ /dev/null @@ -1,5 +0,0 @@ -Odoo has done an update and now, it checks domains of ir.rule on creation and modification. - -The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context. -This field wasn't always set and this caused an error when Odoo checked the domain. -So now it is set to *False* by default. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index a1c84d0a..e7e699db 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,7 +366,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:8a90dec844eac96b44cefb64548fb05283ade12b16246bb89062abc7644e2087 +!! source digest: sha256:439e51bdd708113a0ee8a644f2ca42d29c286af1aee25fa6035b743321969c88 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI @@ -432,17 +431,18 @@

    Odoo FastAPI

  • Known issues / Roadmap
  • Changelog
  • -
  • Bug Tracker
  • -
  • Credits @@ -1702,7 +1702,18 @@

    Known issues / Roadmap

    Changelog

    -

    16.0.1.2.3 (2023-12-21)

    +

    16.0.1.2.5 (2024-01-17)

    +

    Bugfixes

    +
      +
    • Odoo has done an update and now, it checks domains of ir.rule on creation and modification.

      +

      The ir.rule ‘Fastapi: Running user rule’ uses a field (authenticate_partner_id) that comes from the context. +This field wasn’t always set and this caused an error when Odoo checked the domain. +So now it is set to False by default. (#410)

      +
    • +
    +
    +
    +

    16.0.1.2.3 (2023-12-21)

    Bugfixes

    • In case of exception in endpoint execution, close the database cursor after rollback.

      @@ -1711,8 +1722,8 @@

      16.0.1.2.3 (2023-12-21)

    -
    -

    16.0.1.2.2 (2023-12-12)

    +
    +

    16.0.1.2.2 (2023-12-12)

    Bugfixes

    • When using the ‘FastAPITransactionCase’ class, allows to specify a specific @@ -1726,8 +1737,8 @@

      16.0.1.2.2 (2023-12-12)

      ‘authenticated_partner_impl’ method, an error is raised. (#396)
    -
    -

    16.0.1.2.1 (2023-11-03)

    +
    +

    16.0.1.2.1 (2023-11-03)

    Bugfixes

    • Fix a typo in the Field declaration of the ‘count’ attribute of the ‘PagedCollection’ schema.

      @@ -1735,8 +1746,8 @@

      16.0.1.2.1 (2023-11-03)

    -
    -

    16.0.1.2.0 (2023-10-13)

    +
    +

    16.0.1.2.0 (2023-10-13)

    Features

    • The field total in the PagedCollection schema is replaced by the field count. @@ -1750,7 +1761,7 @@

      16.0.1.2.0 (2023-10-13)

    -

    Bug Tracker

    +

    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 @@ -1758,21 +1769,21 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association

    OCA, or the Odoo Community Association, is a nonprofit organization whose From d6f0e32f0b45468afaebc8c206b29b8e04aa9279 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 20 Feb 2024 10:27:27 +0100 Subject: [PATCH 11/12] [FIX] fastapi: Compatibility with latest Odoo From https://github.com/odoo/odoo/commit/cb1d057dcab28cb0b0487244ba99231ee292502e, the orignal werkzeug request is wrapped in a dedicated class to keep under control the attributes developers use. This change add code to make the fastapi addon working with version including this last change and prior version refs #414 --- fastapi/README.rst | 2 +- fastapi/fastapi_dispatcher.py | 15 +++++++++++++-- fastapi/readme/newsfragments/414.bugfix | 6 ++++++ fastapi/static/description/index.html | 3 ++- 4 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 fastapi/readme/newsfragments/414.bugfix diff --git a/fastapi/README.rst b/fastapi/README.rst index 438bf8cd..c287f07d 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:439e51bdd708113a0ee8a644f2ca42d29c286af1aee25fa6035b743321969c88 + !! source digest: sha256:de5a0e0642df22387984fd10d2e54007b88126bacdb22e9e8b637c6187abaeeb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index 904deeff..02738b9b 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -42,8 +42,19 @@ def _make_response(self, status_mapping, headers_tuple, content): self.headers = dict(headers_tuple) def _get_environ(self): - environ = self.request.httprequest.environ - environ["wsgi.input"] = self.request.httprequest._get_stream_for_parsing() + try: + # normal case after + # https://github.com/odoo/odoo/commit/cb1d057dcab28cb0b0487244ba99231ee292502e + httprequest = self.request.httprequest._HTTPRequest__wrapped + except AttributeError: + # fallback for older odoo versions + # The try except is the most efficient way to handle this + # as we expect that most of the time the attribute will be there + # and this code will no more be executed if it runs on an up to + # 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() return environ @contextmanager diff --git a/fastapi/readme/newsfragments/414.bugfix b/fastapi/readme/newsfragments/414.bugfix new file mode 100644 index 00000000..995a1fc1 --- /dev/null +++ b/fastapi/readme/newsfragments/414.bugfix @@ -0,0 +1,6 @@ +Fix compatibility issues with the latest Odoo version + +From https://github.com/odoo/odoo/commit/cb1d057dcab28cb0b0487244ba99231ee292502e +the original werkzeug HTTPRequest class has been wrapped in a new class to keep +under control the attributes developers use. This changes take care of this +new implementation but also keep compatibility with the old ones. diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index e7e699db..6d439c4d 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,3 +1,4 @@ + @@ -366,7 +367,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:439e51bdd708113a0ee8a644f2ca42d29c286af1aee25fa6035b743321969c88 +!! source digest: sha256:de5a0e0642df22387984fd10d2e54007b88126bacdb22e9e8b637c6187abaeeb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From 956c1a391320887194a590e76cb9b72902b9ee8e Mon Sep 17 00:00:00 2001 From: chien Date: Wed, 21 Feb 2024 09:36:13 +0700 Subject: [PATCH 12/12] [MIG] fastapi: Migration to 17.0 --- .pre-commit-config.yaml | 1 - fastapi/README.rst | 2607 ++++++++++++----------- fastapi/__manifest__.py | 4 +- fastapi/models/fastapi_endpoint.py | 34 +- fastapi/models/fastapi_endpoint_demo.py | 7 +- fastapi/pyproject.toml | 3 + fastapi/readme/CONTRIBUTORS.md | 1 + fastapi/readme/CONTRIBUTORS.rst | 1 - fastapi/readme/DESCRIPTION.md | 41 + fastapi/readme/DESCRIPTION.rst | 38 - fastapi/readme/HISTORY.md | 61 + fastapi/readme/HISTORY.rst | 61 - fastapi/readme/ROADMAP.md | 13 + fastapi/readme/ROADMAP.rst | 10 - fastapi/readme/USAGE.md | 1394 ++++++++++++ fastapi/readme/USAGE.rst | 1375 ------------ fastapi/schemas.py | 8 +- fastapi/static/description/index.html | 1241 ++++++----- fastapi/tests/common.py | 7 +- fastapi/views/fastapi_endpoint.xml | 12 +- fastapi/views/fastapi_endpoint_demo.xml | 2 +- requirements.txt | 5 + 22 files changed, 3546 insertions(+), 3380 deletions(-) create mode 100644 fastapi/pyproject.toml create mode 100644 fastapi/readme/CONTRIBUTORS.md delete mode 100644 fastapi/readme/CONTRIBUTORS.rst create mode 100644 fastapi/readme/DESCRIPTION.md delete mode 100644 fastapi/readme/DESCRIPTION.rst create mode 100644 fastapi/readme/HISTORY.md delete mode 100644 fastapi/readme/HISTORY.rst create mode 100644 fastapi/readme/ROADMAP.md delete mode 100644 fastapi/readme/ROADMAP.rst create mode 100644 fastapi/readme/USAGE.md delete mode 100644 fastapi/readme/USAGE.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b67796ba..e6459461 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,6 @@ exclude: | ^datamodel/| ^extendable/| ^extendable_fastapi/| - ^fastapi/| ^fastapi_auth_jwt/| ^fastapi_auth_jwt_demo/| ^graphql_base/| diff --git a/fastapi/README.rst b/fastapi/README.rst index c287f07d..9f0ba9d2 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -17,55 +17,58 @@ Odoo FastAPI :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi + :target: https://github.com/OCA/rest-framework/tree/17.0/fastapi :alt: OCA/rest-framework .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi + :target: https://translation.odoo-community.org/projects/rest-framework-17-0/rest-framework-17-0-fastapi :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| -This addon provides the basis to smoothly integrate the `FastAPI`_ -framework into Odoo. +This addon provides the basis to smoothly integrate the +`FastAPI `__ framework into Odoo. -This integration allows you to use all the goodies from `FastAPI`_ to build custom -APIs for your Odoo server based on standard Python type hints. +This integration allows you to use all the goodies from +`FastAPI `__ to build custom APIs for +your Odoo server based on standard Python type hints. **What is building an API?** -An API is a set of functions that can be called from the outside world. The -goal of an API is to provide a way to interact with your application from the -outside world without having to know how it works internally. A common mistake -when you are building an API is to expose all the internal functions of your -application and therefore create a tight coupling between the outside world and -your internal datamodel and business logic. This is not a good idea because it -makes it very hard to change your internal datamodel and business logic without -breaking the outside world. - -When you are building an API, you define a contract between the outside world -and your application. This contract is defined by the functions that you expose -and the parameters that you accept. This contract is the API. When you change -your internal datamodel and business logic, you can still keep the same API -contract and therefore you don't break the outside world. Even if you change -your implementation, as long as you keep the same API contract, the outside -world will still work. This is the beauty of an API and this is why it is so -important to design a good API. - -A good API is designed to be stable and to be easy to use. It's designed to -provide high-level functions related to a specific use case. It's designed to -be easy to use by hiding the complexity of the internal datamodel and business -logic. A common mistake when you are building an API is to expose all the internal -functions of your application and let the oustide world deal with the complexity -of your internal datamodel and business logic. Don't forget that on a transactional -point of view, each call to an API function is a transaction. This means that -if a specific use case requires multiple calls to your API, you should provide -a single function that does all the work in a single transaction. This why APIs -methods are called high-level and atomic functions. - -.. _FastAPI: https://fastapi.tiangolo.com/ +An API is a set of functions that can be called from the outside world. +The goal of an API is to provide a way to interact with your application +from the outside world without having to know how it works internally. A +common mistake when you are building an API is to expose all the +internal functions of your application and therefore create a tight +coupling between the outside world and your internal datamodel and +business logic. This is not a good idea because it makes it very hard to +change your internal datamodel and business logic without breaking the +outside world. + +When you are building an API, you define a contract between the outside +world and your application. This contract is defined by the functions +that you expose and the parameters that you accept. This contract is the +API. When you change your internal datamodel and business logic, you can +still keep the same API contract and therefore you don't break the +outside world. Even if you change your implementation, as long as you +keep the same API contract, the outside world will still work. This is +the beauty of an API and this is why it is so important to design a good +API. + +A good API is designed to be stable and to be easy to use. It's designed +to provide high-level functions related to a specific use case. It's +designed to be easy to use by hiding the complexity of the internal +datamodel and business logic. A common mistake when you are building an +API is to expose all the internal functions of your application and let +the oustide world deal with the complexity of your internal datamodel +and business logic. Don't forget that on a transactional point of view, +each call to an API function is a transaction. This means that if a +specific use case requires multiple calls to your API, you should +provide a single function that does all the work in a single +transaction. This why APIs methods are called high-level and atomic +functions. **Table of contents** @@ -76,1393 +79,1437 @@ Usage ===== What's building an API with fastapi? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------ -FastAPI is a modern, fast (high-performance), web framework for building APIs -with Python 3.7+ based on standard Python type hints. This addons let's you -keep advantage of the fastapi framework and use it with Odoo. +FastAPI is a modern, fast (high-performance), web framework for building +APIs with Python 3.7+ based on standard Python type hints. This addons +let's you keep advantage of the fastapi framework and use it with Odoo. Before you start, we must define some terms: -* **App**: A FastAPI app is a collection of routes, dependencies, and other - components that can be used to build a web application. -* **Router**: A router is a collection of routes that can be mounted in an - app. -* **Route**: A route is a mapping between an HTTP method and a path, and - defines what should happen when the user requests that path. -* **Dependency**: A dependency is a callable that can be used to get some - information from the user request, or to perform some actions before the - request handler is called. -* **Request**: A request is an object that contains all the information - sent by the user's browser as part of an HTTP request. -* **Response**: A response is an object that contains all the information - that the user's browser needs to build the result page. -* **Handler**: A handler is a function that takes a request and returns a - response. -* **Middleware**: A middleware is a function that takes a request and a - handler, and returns a response. +- **App**: A FastAPI app is a collection of routes, dependencies, and + other components that can be used to build a web application. +- **Router**: A router is a collection of routes that can be mounted in + an app. +- **Route**: A route is a mapping between an HTTP method and a path, + and defines what should happen when the user requests that path. +- **Dependency**: A dependency is a callable that can be used to get + some information from the user request, or to perform some actions + before the request handler is called. +- **Request**: A request is an object that contains all the information + sent by the user's browser as part of an HTTP request. +- **Response**: A response is an object that contains all the + information that the user's browser needs to build the result page. +- **Handler**: A handler is a function that takes a request and returns + a response. +- **Middleware**: A middleware is a function that takes a request and a + handler, and returns a response. The FastAPI framework is based on the following principles: -* **Fast**: Very high performance, on par with NodeJS and Go (thanks to Starlette - and Pydantic). [One of the fastest Python frameworks available] -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. -* **Intuitive**: Great editor support. Completion everywhere. Less time - debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter - declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards - for APIs: OpenAPI (previously known as Swagger) and JSON Schema. -* **Open Source**: FastAPI is fully open-source, under the MIT license. +- **Fast**: Very high performance, on par with NodeJS and Go (thanks to + Starlette and Pydantic). [One of the fastest Python frameworks + available] +- **Fast to code**: Increase the speed to develop features by about + 200% to 300%. +- **Fewer bugs**: Reduce about 40% of human (developer) induced errors. +- **Intuitive**: Great editor support. Completion everywhere. Less time + debugging. +- **Easy**: Designed to be easy to use and learn. Less time reading + docs. +- **Short**: Minimize code duplication. Multiple features from each + parameter declaration. Fewer bugs. +- **Robust**: Get production-ready code. With automatic interactive + documentation. +- **Standards-based**: Based on (and fully compatible with) the open + standards for APIs: OpenAPI (previously known as Swagger) and JSON + Schema. +- **Open Source**: FastAPI is fully open-source, under the MIT license. The first step is to install the fastapi addon. You can do it with the following command: - $ pip install odoo-addon-fastapi + $ pip install odoo-addon-fastapi -Once the addon is installed, you can start building your API. The first thing -you need to do is to create a new addon that depends on 'fastapi'. For example, -let's create an addon called *my_demo_api*. +Once the addon is installed, you can start building your API. The first +thing you need to do is to create a new addon that depends on 'fastapi'. +For example, let's create an addon called *my_demo_api*. -Then, you need to declare your app by defining a model that inherits from -'fastapi.endpoint' and add your app name into the app field. For example: +Then, you need to declare your app by defining a model that inherits +from 'fastapi.endpoint' and add your app name into the app field. For +example: -.. code-block:: python +.. code:: python - from odoo import fields, models + from odoo import fields, models - class FastapiEndpoint(models.Model): + class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" + _inherit = "fastapi.endpoint" - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) -The **'fastapi.endpoint'** model is the base model for all the endpoints. An endpoint -instance is the mount point for a fastapi app into Odoo. When you create a new -endpoint, you can define the app that you want to mount in the **'app'** field -and the path where you want to mount it in the **'path'** field. +The **'fastapi.endpoint'** model is the base model for all the +endpoints. An endpoint instance is the mount point for a fastapi app +into Odoo. When you create a new endpoint, you can define the app that +you want to mount in the **'app'** field and the path where you want to +mount it in the **'path'** field. figure:: static/description/endpoint_create.png - FastAPI Endpoint + FastAPI Endpoint -Thanks to the **'fastapi.endpoint'** model, you can create as many endpoints as -you want and mount as many apps as you want in each endpoint. The endpoint is -also the place where you can define configuration parameters for your app. A -typical example is the authentication method that you want to use for your app -when accessed at the endpoint path. +Thanks to the **'fastapi.endpoint'** model, you can create as many +endpoints as you want and mount as many apps as you want in each +endpoint. The endpoint is also the place where you can define +configuration parameters for your app. A typical example is the +authentication method that you want to use for your app when accessed at +the endpoint path. -Now, you can create your first router. For that, you need to define a global -variable into your fastapi_endpoint module called for example 'demo_api_router' +Now, you can create your first router. For that, you need to define a +global variable into your fastapi_endpoint module called for example +'demo_api_router' -.. code-block:: python +.. code:: python - from fastapi import APIRouter - from odoo import fields, models + from fastapi import APIRouter + from odoo import fields, models - class FastapiEndpoint(models.Model): + class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" + _inherit = "fastapi.endpoint" - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) - # create a router - demo_api_router = APIRouter() + # create a router + demo_api_router = APIRouter() +To make your router available to your app, you need to add it to the +list of routers returned by the **\_get_fastapi_routers** method of your +fastapi_endpoint model. -To make your router available to your app, you need to add it to the list of routers -returned by the **_get_fastapi_routers** method of your fastapi_endpoint model. +.. code:: python -.. code-block:: python + from fastapi import APIRouter + from odoo import api, fields, models - from fastapi import APIRouter - from odoo import api, fields, models + class FastapiEndpoint(models.Model): - class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" - _inherit = "fastapi.endpoint" + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) + def _get_fastapi_routers(self): + if self.app == "demo": + return [demo_api_router] + return super()._get_fastapi_routers() - def _get_fastapi_routers(self): - if self.app == "demo": - return [demo_api_router] - return super()._get_fastapi_routers() + # create a router + demo_api_router = APIRouter() - # create a router - demo_api_router = APIRouter() +Now, you can start adding routes to your router. For example, let's add +a route that returns a list of partners. -Now, you can start adding routes to your router. For example, let's add a route -that returns a list of partners. +.. code:: python -.. code-block:: python + from typing import Annotated - from typing import Annotated + from fastapi import APIRouter + from pydantic import BaseModel - from fastapi import APIRouter - from pydantic import BaseModel + from odoo import api, fields, models + from odoo.api import Environment - from odoo import api, fields, models - from odoo.api import Environment + from odoo.addons.fastapi.dependencies import odoo_env - from odoo.addons.fastapi.dependencies import odoo_env + class FastapiEndpoint(models.Model): - class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" - _inherit = "fastapi.endpoint" + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) + def _get_fastapi_routers(self): + if self.app == "demo": + return [demo_api_router] + return super()._get_fastapi_routers() - def _get_fastapi_routers(self): - if self.app == "demo": - return [demo_api_router] - return super()._get_fastapi_routers() + # create a router + demo_api_router = APIRouter() - # create a router - demo_api_router = APIRouter() + class PartnerInfo(BaseModel): + name: str + email: str - class PartnerInfo(BaseModel): - name: str - email: str + @demo_api_router.get("/partners", response_model=list[PartnerInfo]) + def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: + return [ + PartnerInfo(name=partner.name, email=partner.email) + for partner in env["res.partner"].search([]) + ] - @demo_api_router.get("/partners", response_model=list[PartnerInfo]) - def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: - return [ - PartnerInfo(name=partner.name, email=partner.email) - for partner in env["res.partner"].search([]) - ] +Now, you can start your Odoo server, install your addon and create a new +endpoint instance for your app. Once it's done click on the docs url to +access the interactive documentation of your app. -Now, you can start your Odoo server, install your addon and create a new endpoint -instance for your app. Once it's done click on the docs url to access the -interactive documentation of your app. +Before trying to test your app, you need to define on the endpoint +instance the user that will be used to run the app. You can do it by +setting the **'user_id'** field. This information is the most important +one because it's the basis for the security of your app. The user that +you define in the endpoint instance will be used to run the app and to +access the database. This means that the user will be able to access all +the data that he has access to in Odoo. To ensure the security of your +app, you should create a new user that will be used only to run your app +and that will have no access to the database. -Before trying to test your app, you need to define on the endpoint instance the -user that will be used to run the app. You can do it by setting the **'user_id'** -field. This information is the most important one because it's the basis for -the security of your app. The user that you define in the endpoint instance -will be used to run the app and to access the database. This means that the -user will be able to access all the data that he has access to in Odoo. To ensure -the security of your app, you should create a new user that will be used only -to run your app and that will have no access to the database. +.. code:: xml -.. code-block:: xml + + My Demo Endpoint User + my_demo_app_user + + - - My Demo Endpoint User - my_demo_app_user - - +At the same time you should create a new group that will be used to +define the access rights of the user that will run your app. This group +should imply the predefined group **'FastAPI Endpoint Runner'**. This +group defines the minimum access rights that the user needs to: -At the same time you should create a new group that will be used to define the -access rights of the user that will run your app. This group should imply -the predefined group **'FastAPI Endpoint Runner'**. This group defines the -minimum access rights that the user needs to: +- access the endpoint instance it belongs to +- access to its own user record +- access to the partner record that is linked to its user record -* access the endpoint instance it belongs to -* access to its own user record -* access to the partner record that is linked to its user record +.. code:: xml -.. code-block:: xml + + My Demo Endpoint Group + + + - - My Demo Endpoint Group - - - +Now, you can test your app. You can do it by clicking on the 'Try it +out' button of the route that you have defined. The result of the +request will be displayed in the 'Response' section and contains the +list of partners. + +Note - -Now, you can test your app. You can do it by clicking on the 'Try it out' button -of the route that you have defined. The result of the request will be displayed -in the 'Response' section and contains the list of partners. - -.. note:: - The **'FastAPI Endpoint Runner'** group ensures that the user cannot access any - information others than the 3 ones mentioned above. This means that for every - information that you want to access from your app, you need to create the - proper ACLs and record rules. (see `Managing security into the route handlers`_) - It's a good practice to use a dedicated user into a specific group from the - beginning of your project and in your tests. This will force you to define - the proper security rules for your endoints. +The **'FastAPI Endpoint Runner'** group ensures that the user cannot +access any information others than the 3 ones mentioned above. This +means that for every information that you want to access from your app, +you need to create the proper ACLs and record rules. (see `Managing +security into the route +handlers <#managing-security-into-the-route-handlers>`__) It's a good +practice to use a dedicated user into a specific group from the +beginning of your project and in your tests. This will force you to +define the proper security rules for your endoints. Dealing with the odoo environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The **'odoo.addons.fastapi.dependencies'** module provides a set of functions that you can use -to inject reusable dependencies into your routes. For example, the **'odoo_env'** -function returns the current odoo environment. You can use it to access the -odoo models and the database from your route handlers. - -.. code-block:: python - - from typing import Annotated - - from odoo.api import Environment - from odoo.addons.fastapi.dependencies import odoo_env - - @demo_api_router.get("/partners", response_model=list[PartnerInfo]) - def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: - return [ - PartnerInfo(name=partner.name, email=partner.email) - for partner in env["res.partner"].search([]) - ] - -As you can see, you can use the **'Depends'** function to inject the dependency -into your route handler. The **'Depends'** function is provided by the -**'fastapi'** framework. You can use it to inject any dependency into your route -handler. As your handler is a python function, the only way to get access to -the odoo environment is to inject it as a dependency. The fastapi addon provides -a set of function that can be used as dependencies: - -* **'odoo_env'**: Returns the current odoo environment. -* **'fastapi_endpoint'**: Returns the current fastapi endpoint model instance. -* **'authenticated_partner'**: Returns the authenticated partner. -* **'authenticated_partner_env'**: Returns the current odoo environment with the - authenticated_partner_id into the context. - -By default, the **'odoo_env'** and **'fastapi_endpoint'** dependencies are -available without extra work. - -.. note:: - Even if 'odoo_env' and 'authenticated_partner_env' returns the current odoo - environment, they are not the same. The 'odoo_env' dependency returns the - environment without any modification while the 'authenticated_partner_env' - adds the authenticated partner id into the context of the environment. As it will - be explained in the section `Managing security into the route handlers`_ dedicated - to the security, the presence of the authenticated partner id into the context - is the key information that will allow you to enforce the security of your endpoint - methods. As consequence, you should always use the 'authenticated_partner_env' - dependency instead of the 'odoo_env' dependency for all the methods that are - not public. +--------------------------------- + +The **'odoo.addons.fastapi.dependencies'** module provides a set of +functions that you can use to inject reusable dependencies into your +routes. For example, the **'odoo_env'** function returns the current +odoo environment. You can use it to access the odoo models and the +database from your route handlers. + +.. code:: python + + from typing import Annotated + + from odoo.api import Environment + from odoo.addons.fastapi.dependencies import odoo_env + + @demo_api_router.get("/partners", response_model=list[PartnerInfo]) + def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: + return [ + PartnerInfo(name=partner.name, email=partner.email) + for partner in env["res.partner"].search([]) + ] + +As you can see, you can use the **'Depends'** function to inject the +dependency into your route handler. The **'Depends'** function is +provided by the **'fastapi'** framework. You can use it to inject any +dependency into your route handler. As your handler is a python +function, the only way to get access to the odoo environment is to +inject it as a dependency. The fastapi addon provides a set of function +that can be used as dependencies: + +- **'odoo_env'**: Returns the current odoo environment. +- **'fastapi_endpoint'**: Returns the current fastapi endpoint model + instance. +- **'authenticated_partner'**: Returns the authenticated partner. +- **'authenticated_partner_env'**: Returns the current odoo environment + with the authenticated_partner_id into the context. + +By default, the **'odoo_env'** and **'fastapi_endpoint'** dependencies +are available without extra work. + +Note + +Even if 'odoo_env' and 'authenticated_partner_env' returns the current +odoo environment, they are not the same. The 'odoo_env' dependency +returns the environment without any modification while the +'authenticated_partner_env' adds the authenticated partner id into the +context of the environment. As it will be explained in the section +`Managing security into the route +handlers <#managing-security-into-the-route-handlers>`__ dedicated to +the security, the presence of the authenticated partner id into the +context is the key information that will allow you to enforce the +security of your endpoint methods. As consequence, you should always use +the 'authenticated_partner_env' dependency instead of the 'odoo_env' +dependency for all the methods that are not public. The dependency injection mechanism -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The **'odoo_env'** dependency relies on a simple implementation that retrieves -the current odoo environment from ContextVar variable initialized at the start -of the request processing by the specific request dispatcher processing the -fastapi requests. - -The **'fastapi_endpoint'** dependency relies on the 'dependency_overrides' mechanism -provided by the **'fastapi'** module. (see the fastapi documentation for more -details about the dependency_overrides mechanism). If you take a look at the -current implementation of the **'fastapi_endpoint'** dependency, you will see -that the method depends of two parameters: **'endpoint_id'** and **'env'**. Each -of these parameters are dependencies themselves. - -.. code-block:: python - - def fastapi_endpoint_id() -> int: - """This method is overriden by default to make the fastapi.endpoint record - available for your endpoint method. To get the fastapi.endpoint record - in your method, you just need to add a dependency on the fastapi_endpoint method - defined below - """ - - - def fastapi_endpoint( - _id: Annotated[int, Depends(fastapi_endpoint_id)], - env: Annotated[Environment, Depends(odoo_env)], - ) -> "FastapiEndpoint": - """Return the fastapi.endpoint record""" - return env["fastapi.endpoint"].browse(_id) +---------------------------------- + +The **'odoo_env'** dependency relies on a simple implementation that +retrieves the current odoo environment from ContextVar variable +initialized at the start of the request processing by the specific +request dispatcher processing the fastapi requests. + +The **'fastapi_endpoint'** dependency relies on the +'dependency_overrides' mechanism provided by the **'fastapi'** module. +(see the fastapi documentation for more details about the +dependency_overrides mechanism). If you take a look at the current +implementation of the **'fastapi_endpoint'** dependency, you will see +that the method depends of two parameters: **'endpoint_id'** and +**'env'**. Each of these parameters are dependencies themselves. + +.. code:: python + + def fastapi_endpoint_id() -> int: + """This method is overriden by default to make the fastapi.endpoint record + available for your endpoint method. To get the fastapi.endpoint record + in your method, you just need to add a dependency on the fastapi_endpoint method + defined below + """ + + + def fastapi_endpoint( + _id: Annotated[int, Depends(fastapi_endpoint_id)], + env: Annotated[Environment, Depends(odoo_env)], + ) -> "FastapiEndpoint": + """Return the fastapi.endpoint record""" + return env["fastapi.endpoint"].browse(_id) + +As you can see, one of these dependencies is the +**'fastapi_endpoint_id'** dependency and has no concrete implementation. +This method is used as a contract that must be implemented/provided at +the time the fastapi app is created. Here comes the power of the +dependency_overrides mechanism. + +If you take a look at the **'\_get_app'** method of the +**'FastapiEndpoint'** model, you will see that the +**'fastapi_endpoint_id'** dependency is overriden by registering a +specific method that returns the id of the current fastapi endpoint +model instance for the original method. +.. code:: python -As you can see, one of these dependencies is the **'fastapi_endpoint_id'** -dependency and has no concrete implementation. This method is used as a contract -that must be implemented/provided at the time the fastapi app is created. -Here comes the power of the dependency_overrides mechanism. + def _get_app(self) -> FastAPI: + app = FastAPI(**self._prepare_fastapi_endpoint_params()) + for router in self._get_fastapi_routers(): + app.include_router(prefix=self.root_path, router=router) + app.dependency_overrides[dependencies.fastapi_endpoint_id] = partial( + lambda a: a, self.id + ) -If you take a look at the **'_get_app'** method of the **'FastapiEndpoint'** model, -you will see that the **'fastapi_endpoint_id'** dependency is overriden by -registering a specific method that returns the id of the current fastapi endpoint -model instance for the original method. +This kind of mechanism is very powerful and allows you to inject any +dependency into your route handlers and moreover, define an abstract +dependency that can be used by any other addon and for which the +implementation could depend on the endpoint configuration. -.. code-block:: python +The authentication mechanism +---------------------------- - def _get_app(self) -> FastAPI: - app = FastAPI(**self._prepare_fastapi_endpoint_params()) - for router in self._get_fastapi_routers(): - app.include_router(prefix=self.root_path, router=router) - app.dependency_overrides[dependencies.fastapi_endpoint_id] = partial( - lambda a: a, self.id - ) +To make our app not tightly coupled with a specific authentication +mechanism, we will use the **'authenticated_partner'** dependency. As +for the **'fastapi_endpoint'** this dependency depends on an abstract +dependency. -This kind of mechanism is very powerful and allows you to inject any dependency -into your route handlers and moreover, define an abstract dependency that can be -used by any other addon and for which the implementation could depend on the -endpoint configuration. +When you define a route handler, you can inject the +**'authenticated_partner'** dependency as a parameter of your route +handler. -The authentication mechanism -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To make our app not tightly coupled with a specific authentication mechanism, -we will use the **'authenticated_partner'** dependency. As for the -**'fastapi_endpoint'** this dependency depends on an abstract dependency. - -When you define a route handler, you can inject the **'authenticated_partner'** -dependency as a parameter of your route handler. - -.. code-block:: python - - from odoo.addons.base.models.res_partner import Partner - - - @demo_api_router.get("/partners", response_model=list[PartnerInfo]) - def get_partners( - env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)] - ) -> list[PartnerInfo]: - return [ - PartnerInfo(name=partner.name, email=partner.email) - for partner in env["res.partner"].search([]) - ] - - -At this stage, your handler is not tied to a specific authentication mechanism -but only expects to get a partner as a dependency. Depending on your needs, you -can implement different authentication mechanism available for your app. -The fastapi addon provides a default authentication mechanism using the -'BasicAuth' method. This authentication mechanism is implemented in the -**'odoo.addons.fastapi.dependencies'** module and relies on functionalities provided -by the **'fastapi.security'** module. - -.. code-block:: python - - def authenticated_partner( - env: Annotated[Environment, Depends(odoo_env)], - security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())], - ) -> "res.partner": - """Return the authenticated partner""" - partner = env["res.partner"].search( - [("email", "=", security.username)], limit=1 - ) - if not partner: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Basic"}, - ) - if not partner.check_password(security.password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Basic"}, - ) - return partner +.. code:: python + + from odoo.addons.base.models.res_partner import Partner + + + @demo_api_router.get("/partners", response_model=list[PartnerInfo]) + def get_partners( + env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)] + ) -> list[PartnerInfo]: + return [ + PartnerInfo(name=partner.name, email=partner.email) + for partner in env["res.partner"].search([]) + ] + +At this stage, your handler is not tied to a specific authentication +mechanism but only expects to get a partner as a dependency. Depending +on your needs, you can implement different authentication mechanism +available for your app. The fastapi addon provides a default +authentication mechanism using the 'BasicAuth' method. This +authentication mechanism is implemented in the +**'odoo.addons.fastapi.dependencies'** module and relies on +functionalities provided by the **'fastapi.security'** module. + +.. code:: python + + def authenticated_partner( + env: Annotated[Environment, Depends(odoo_env)], + security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())], + ) -> "res.partner": + """Return the authenticated partner""" + partner = env["res.partner"].search( + [("email", "=", security.username)], limit=1 + ) + if not partner: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + if not partner.check_password(security.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return partner As you can see, the **'authenticated_partner'** dependency relies on the -**'HTTPBasic'** dependency provided by the **'fastapi.security'** module. -In this dummy implementation, we just check that the provided credentials -can be used to authenticate a user in odoo. If the authentication is successful, -we return the partner record linked to the authenticated user. - -In some cases you could want to implement a more complex authentication mechanism -that could rely on a token or a session. In this case, you can override the -**'authenticated_partner'** dependency by registering a specific method that -returns the authenticated partner. Moreover, you can make it configurable on -the fastapi endpoint model instance. +**'HTTPBasic'** dependency provided by the **'fastapi.security'** +module. In this dummy implementation, we just check that the provided +credentials can be used to authenticate a user in odoo. If the +authentication is successful, we return the partner record linked to the +authenticated user. + +In some cases you could want to implement a more complex authentication +mechanism that could rely on a token or a session. In this case, you can +override the **'authenticated_partner'** dependency by registering a +specific method that returns the authenticated partner. Moreover, you +can make it configurable on the fastapi endpoint model instance. To do it, you just need to implement a specific method for each of your -authentication mechanism and allows the user to select one of these methods -when he creates a new fastapi endpoint. Let's say that we want to allow the -authentication by using an api key or via basic auth. Since basic auth is already -implemented, we will only implement the api key authentication mechanism. - -.. code-block:: python - - from fastapi.security import APIKeyHeader - - def api_key_based_authenticated_partner_impl( - api_key: Annotated[str, Depends( - APIKeyHeader( - name="api-key", - description="In this demo, you can use a user's login as api key.", - ) - )], - env: Annotated[Environment, Depends(odoo_env)], - ) -> Partner: - """A dummy implementation that look for a user with the same login - as the provided api key - """ - partner = env["res.users"].search([("login", "=", api_key)], limit=1).partner_id - if not partner: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key" - ) - return partner - -As for the 'BasicAuth' authentication mechanism, we also rely on one of the native -security dependency provided by the **'fastapi.security'** module. - -Now that we have an implementation for our two authentication mechanisms, we -can allows the user to select one of these authentication mechanisms by adding -a selection field on the fastapi endpoint model. - -.. code-block:: python - - from odoo import fields, models - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - demo_auth_method = fields.Selection( - selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], - string="Authenciation method", - ) - -.. note:: - A good practice is to prefix specific configuration fields of your app with - the name of your app. This will avoid conflicts with other app when the - 'fastapi.endpoint' model is extended for other 'app'. +authentication mechanism and allows the user to select one of these +methods when he creates a new fastapi endpoint. Let's say that we want +to allow the authentication by using an api key or via basic auth. Since +basic auth is already implemented, we will only implement the api key +authentication mechanism. + +.. code:: python + + from fastapi.security import APIKeyHeader + + def api_key_based_authenticated_partner_impl( + api_key: Annotated[str, Depends( + APIKeyHeader( + name="api-key", + description="In this demo, you can use a user's login as api key.", + ) + )], + env: Annotated[Environment, Depends(odoo_env)], + ) -> Partner: + """A dummy implementation that look for a user with the same login + as the provided api key + """ + partner = env["res.users"].search([("login", "=", api_key)], limit=1).partner_id + if not partner: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key" + ) + return partner + +As for the 'BasicAuth' authentication mechanism, we also rely on one of +the native security dependency provided by the **'fastapi.security'** +module. + +Now that we have an implementation for our two authentication +mechanisms, we can allows the user to select one of these authentication +mechanisms by adding a selection field on the fastapi endpoint model. + +.. code:: python + + from odoo import fields, models + + class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], + string="Authenciation method", + ) + +Note + +A good practice is to prefix specific configuration fields of your app +with the name of your app. This will avoid conflicts with other app when +the 'fastapi.endpoint' model is extended for other 'app'. Now that we have a selection field that allows the user to select the authentication method, we can use the dependency override mechanism to -provide the right implementation of the **'authenticated_partner'** dependency -when the app is instantiated. - -.. code-block:: python - - from odoo.addons.fastapi.dependencies import authenticated_partner - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - demo_auth_method = fields.Selection( - selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], - string="Authenciation method", - ) - - def _get_app(self) -> FastAPI: - app = super()._get_app() - if self.app == "demo": - # Here we add the overrides to the authenticated_partner_impl method - # according to the authentication method configured on the demo app - if self.demo_auth_method == "http_basic": - authenticated_partner_impl_override = ( - authenticated_partner_from_basic_auth_user - ) - else: - authenticated_partner_impl_override = ( - api_key_based_authenticated_partner_impl - ) - app.dependency_overrides[ - authenticated_partner_impl - ] = authenticated_partner_impl_override - return app - - -To see how the dependency override mechanism works, you can take a look at the -demo app provided by the fastapi addon. If you choose the app 'demo' in the -fastapi endpoint form view, you will see that the authentication method -is configurable. You can also see that depending on the authentication method -configured on your fastapi endpoint, the documentation will change. - -.. note:: - At time of writing, the dependency override mechanism is not supported by - the fastapi documentation generator. A fix has been proposed and is waiting - to be merged. You can follow the progress of the fix on `github - `_ +provide the right implementation of the **'authenticated_partner'** +dependency when the app is instantiated. + +.. code:: python + + from odoo.addons.fastapi.dependencies import authenticated_partner + class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], + string="Authenciation method", + ) + + def _get_app(self) -> FastAPI: + app = super()._get_app() + if self.app == "demo": + # Here we add the overrides to the authenticated_partner_impl method + # according to the authentication method configured on the demo app + if self.demo_auth_method == "http_basic": + authenticated_partner_impl_override = ( + authenticated_partner_from_basic_auth_user + ) + else: + authenticated_partner_impl_override = ( + api_key_based_authenticated_partner_impl + ) + app.dependency_overrides[ + authenticated_partner_impl + ] = authenticated_partner_impl_override + return app + +To see how the dependency override mechanism works, you can take a look +at the demo app provided by the fastapi addon. If you choose the app +'demo' in the fastapi endpoint form view, you will see that the +authentication method is configurable. You can also see that depending +on the authentication method configured on your fastapi endpoint, the +documentation will change. + +Note + +At time of writing, the dependency override mechanism is not supported +by the fastapi documentation generator. A fix has been proposed and is +waiting to be merged. You can follow the progress of the fix on +`github `__ Managing configuration parameters for your app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -As we have seen in the previous section, you can add configuration fields -on the fastapi endpoint model to allow the user to configure your app (as for -any odoo model you extend). When you need to access these configuration fields -in your route handlers, you can use the **'odoo.addons.fastapi.dependencies.fastapi_endpoint'** -dependency method to retrieve the 'fastapi.endpoint' record associated to the +---------------------------------------------- + +As we have seen in the previous section, you can add configuration +fields on the fastapi endpoint model to allow the user to configure your +app (as for any odoo model you extend). When you need to access these +configuration fields in your route handlers, you can use the +**'odoo.addons.fastapi.dependencies.fastapi_endpoint'** dependency +method to retrieve the 'fastapi.endpoint' record associated to the current request. -.. code-block:: python - - from pydantic import BaseModel, Field - from odoo.addons.fastapi.dependencies import fastapi_endpoint - - class EndpointAppInfo(BaseModel): - id: str - name: str - app: str - auth_method: str = Field(alias="demo_auth_method") - root_path: str - model_config = ConfigDict(from_attributes=True) - - - @demo_api_router.get( - "/endpoint_app_info", - response_model=EndpointAppInfo, - dependencies=[Depends(authenticated_partner)], - ) - async def endpoint_app_info( - endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], - ) -> EndpointAppInfo: - """Returns the current endpoint configuration""" - # This method show you how to get access to current endpoint configuration - # It also show you how you can specify a dependency to force the security - # even if the method doesn't require the authenticated partner as parameter - return EndpointAppInfo.model_validate(endpoint) - -Some of the configuration fields of the fastapi endpoint could impact the way -the app is instantiated. For example, in the previous section, we have seen -that the authentication method configured on the 'fastapi.endpoint' record is -used in order to provide the right implementation of the **'authenticated_partner'** -when the app is instantiated. To ensure that the app is re-instantiated when -an element of the configuration used in the instantiation of the app is -modified, you must override the **'_fastapi_app_fields'** method to add the -name of the fields that impact the instantiation of the app into the returned +.. code:: python + + from pydantic import BaseModel, Field + from odoo.addons.fastapi.dependencies import fastapi_endpoint + + class EndpointAppInfo(BaseModel): + id: str + name: str + app: str + auth_method: str = Field(alias="demo_auth_method") + root_path: str + model_config = ConfigDict(from_attributes=True) + + + @demo_api_router.get( + "/endpoint_app_info", + response_model=EndpointAppInfo, + dependencies=[Depends(authenticated_partner)], + ) + async def endpoint_app_info( + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + ) -> EndpointAppInfo: + """Returns the current endpoint configuration""" + # This method show you how to get access to current endpoint configuration + # It also show you how you can specify a dependency to force the security + # even if the method doesn't require the authenticated partner as parameter + return EndpointAppInfo.model_validate(endpoint) + +Some of the configuration fields of the fastapi endpoint could impact +the way the app is instantiated. For example, in the previous section, +we have seen that the authentication method configured on the +'fastapi.endpoint' record is used in order to provide the right +implementation of the **'authenticated_partner'** when the app is +instantiated. To ensure that the app is re-instantiated when an element +of the configuration used in the instantiation of the app is modified, +you must override the **'\_fastapi_app_fields'** method to add the name +of the fields that impact the instantiation of the app into the returned list. -.. code-block:: python +.. code:: python - class FastapiEndpoint(models.Model): + class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" + _inherit = "fastapi.endpoint" - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - demo_auth_method = fields.Selection( - selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], - string="Authenciation method", - ) + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], + string="Authenciation method", + ) - @api.model - def _fastapi_app_fields(self) -> List[str]: - fields = super()._fastapi_app_fields() - fields.append("demo_auth_method") - return fields + @api.model + def _fastapi_app_fields(self) -> List[str]: + fields = super()._fastapi_app_fields() + fields.append("demo_auth_method") + return fields Dealing with languages -~~~~~~~~~~~~~~~~~~~~~~ - -The fastapi addon parses the Accept-Language header of the request to determine -the language to use. This parsing is done by respecting the `RFC 7231 specification -`_. That means that -the language is determined by the first language found in the header that is -supported by odoo (with care of the priority order). If no language is found in -the header, the odoo default language is used. This language is then used to -initialize the Odoo's environment context used by the route handlers. All this -makes the management of languages very easy. You don't have to worry about. This -feature is also documented by default into the generated openapi documentation -of your app to instruct the api consumers how to request a specific language. - +---------------------- + +The fastapi addon parses the Accept-Language header of the request to +determine the language to use. This parsing is done by respecting the +`RFC 7231 +specification `__. +That means that the language is determined by the first language found +in the header that is supported by odoo (with care of the priority +order). If no language is found in the header, the odoo default language +is used. This language is then used to initialize the Odoo's environment +context used by the route handlers. All this makes the management of +languages very easy. You don't have to worry about. This feature is also +documented by default into the generated openapi documentation of your +app to instruct the api consumers how to request a specific language. How to extend an existing app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------- When you develop a fastapi app, in a native python app it's not possible -to extend an existing one. This limitation doesn't apply to the fastapi addon -because the fastapi endpoint model is designed to be extended. However, the -way to extend an existing app is not the same as the way to extend an odoo model. - -First of all, it's important to keep in mind that when you define a route, you -are actually defining a contract between the client and the server. This -contract is defined by the route path, the method (GET, POST, PUT, DELETE, -etc.), the parameters and the response. If you want to extend an existing app, -you must ensure that the contract is not broken. Any change to the contract -will respect the `Liskov substitution principle -`_. This means -that the client should not be impacted by the change. - -What does it mean in practice? It means that you can't change the route path -or the method of an existing route. You can't change the name of a parameter -or the type of a response. You can't add a new parameter or a new response. -You can't remove a parameter or a response. If you want to change the contract, -you must create a new route. +to extend an existing one. This limitation doesn't apply to the fastapi +addon because the fastapi endpoint model is designed to be extended. +However, the way to extend an existing app is not the same as the way to +extend an odoo model. + +First of all, it's important to keep in mind that when you define a +route, you are actually defining a contract between the client and the +server. This contract is defined by the route path, the method (GET, +POST, PUT, DELETE, etc.), the parameters and the response. If you want +to extend an existing app, you must ensure that the contract is not +broken. Any change to the contract will respect the `Liskov substitution +principle `__. +This means that the client should not be impacted by the change. + +What does it mean in practice? It means that you can't change the route +path or the method of an existing route. You can't change the name of a +parameter or the type of a response. You can't add a new parameter or a +new response. You can't remove a parameter or a response. If you want to +change the contract, you must create a new route. What can you change? -* You can change the implementation of the route handler. -* You can override the dependencies of the route handler. -* You can add a new route handler. -* You can extend the model used as parameter or as response of the route handler. +- You can change the implementation of the route handler. +- You can override the dependencies of the route handler. +- You can add a new route handler. +- You can extend the model used as parameter or as response of the + route handler. Let's see how to do that. Changing the implementation of the route handler -================================================ - -Let's say that you want to change the implementation of the route handler -**'/demo/echo'**. Since a route handler is just a python method, it could seems -a tedious task since we are not into a model method and therefore we can't -take advantage of the Odoo inheritance mechanism. - -However, the fastapi addon provides a way to do that. Thanks to the **'odoo_env'** -dependency method, you can access the current odoo environment. With this -environment, you can access the registry and therefore the model you want to -delegate the implementation to. If you want to change the implementation of -the route handler **'/demo/echo'**, the only thing you have to do is to -inherit from the model where the implementation is defined and override the -method **'echo'**. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python +Let's say that you want to change the implementation of the route +handler **'/demo/echo'**. Since a route handler is just a python method, +it could seems a tedious task since we are not into a model method and +therefore we can't take advantage of the Odoo inheritance mechanism. - from pydantic import BaseModel - from fastapi import Depends, APIRouter - from odoo import models - from odoo.addons.fastapi.dependencies import odoo_env +However, the fastapi addon provides a way to do that. Thanks to the +**'odoo_env'** dependency method, you can access the current odoo +environment. With this environment, you can access the registry and +therefore the model you want to delegate the implementation to. If you +want to change the implementation of the route handler **'/demo/echo'**, +the only thing you have to do is to inherit from the model where the +implementation is defined and override the method **'echo'**. - class FastapiEndpoint(models.Model): +.. code:: python - _inherit = "fastapi.endpoint" + from pydantic import BaseModel + from fastapi import Depends, APIRouter + from odoo import models + from odoo.addons.fastapi.dependencies import odoo_env - def _get_fastapi_routers(self) -> List[APIRouter]: - routers = super()._get_fastapi_routers() - routers.append(demo_api_router) - return routers + class FastapiEndpoint(models.Model): - demo_api_router = APIRouter() + _inherit = "fastapi.endpoint" - @demo_api_router.get( - "/echo", - response_model=EchoResponse, - dependencies=[Depends(odoo_env)], - ) - async def echo( - message: str, - odoo_env: Annotated[Environment, Depends(odoo_env)], - ) -> EchoResponse: - """Echo the message""" - return EchoResponse(message=odoo_env["demo.fastapi.endpoint"].echo(message)) + def _get_fastapi_routers(self) -> List[APIRouter]: + routers = super()._get_fastapi_routers() + routers.append(demo_api_router) + return routers - class EchoResponse(BaseModel): - message: str + demo_api_router = APIRouter() - class DemoEndpoint(models.AbstractModel): + @demo_api_router.get( + "/echo", + response_model=EchoResponse, + dependencies=[Depends(odoo_env)], + ) + async def echo( + message: str, + odoo_env: Annotated[Environment, Depends(odoo_env)], + ) -> EchoResponse: + """Echo the message""" + return EchoResponse(message=odoo_env["demo.fastapi.endpoint"].echo(message)) - _name = "demo.fastapi.endpoint" - _description = "Demo Endpoint" + class EchoResponse(BaseModel): + message: str - def echo(self, message: str) -> str: - return message + class DemoEndpoint(models.AbstractModel): - class DemoEndpointInherit(models.AbstractModel): + _name = "demo.fastapi.endpoint" + _description = "Demo Endpoint" - _inherit = "demo.fastapi.endpoint" + def echo(self, message: str) -> str: + return message - def echo(self, message: str) -> str: - return f"Hello {message}" + class DemoEndpointInherit(models.AbstractModel): + _inherit = "demo.fastapi.endpoint" -.. note:: + def echo(self, message: str) -> str: + return f"Hello {message}" - It's a good programming practice to implement the business logic outside - the route handler. This way, you can easily test your business logic without - having to test the route handler. In the example above, the business logic - is implemented in the method **'echo'** of the model **'demo.fastapi.endpoint'**. - The route handler just delegate the implementation to this method. +Note +It's a good programming practice to implement the business logic outside +the route handler. This way, you can easily test your business logic +without having to test the route handler. In the example above, the +business logic is implemented in the method **'echo'** of the model +**'demo.fastapi.endpoint'**. The route handler just delegate the +implementation to this method. Overriding the dependencies of the route handler -================================================ - -As you've previously seen, the dependency injection mechanism of fastapi is -very powerful. By designing your route handler to rely on dependencies with -a specific functional scope, you can easily change the implementation of the -dependency without having to change the route handler. With such a design, you -can even define abstract dependencies that must be implemented by the concrete -application. This is the case of the **'authenticated_partner'** dependency in our -previous example. (you can find the implementation of this dependency in the -file **'odoo/addons/fastapi/dependencies.py'** and it's usage in the file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you've previously seen, the dependency injection mechanism of fastapi +is very powerful. By designing your route handler to rely on +dependencies with a specific functional scope, you can easily change the +implementation of the dependency without having to change the route +handler. With such a design, you can even define abstract dependencies +that must be implemented by the concrete application. This is the case +of the **'authenticated_partner'** dependency in our previous example. +(you can find the implementation of this dependency in the file +**'odoo/addons/fastapi/dependencies.py'** and it's usage in the file **'odoo/addons/fastapi/models/fastapi_endpoint_demo.py'**) Adding a new route handler -========================== +~~~~~~~~~~~~~~~~~~~~~~~~~~ Let's say that you want to add a new route handler **'/demo/echo2'**. You could be tempted to add this new route handler in your new addons by -importing the router of the existing app and adding the new route handler to -it. - -.. code-block:: python - - from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router - - @demo_api_router.get( - "/echo2", - response_model=EchoResponse, - dependencies=[Depends(odoo_env)], - ) - async def echo2( - message: str, - odoo_env: Annotated[Environment, Depends(odoo_env)], - ) -> EchoResponse: - """Echo the message""" - echo = odoo_env["demo.fastapi.endpoint"].echo2(message) - return EchoResponse(message=f"Echo2: {echo}") - -The problem with this approach is that you unconditionally add the new route -handler to the existing app even if the app is called for a different database -where your new addon is not installed. - -The solution is to define a new router and to add it to the list of routers -returned by the method **'_get_fastapi_routers'** of the model +importing the router of the existing app and adding the new route +handler to it. + +.. code:: python + + from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router + + @demo_api_router.get( + "/echo2", + response_model=EchoResponse, + dependencies=[Depends(odoo_env)], + ) + async def echo2( + message: str, + odoo_env: Annotated[Environment, Depends(odoo_env)], + ) -> EchoResponse: + """Echo the message""" + echo = odoo_env["demo.fastapi.endpoint"].echo2(message) + return EchoResponse(message=f"Echo2: {echo}") + +The problem with this approach is that you unconditionally add the new +route handler to the existing app even if the app is called for a +different database where your new addon is not installed. + +The solution is to define a new router and to add it to the list of +routers returned by the method **'\_get_fastapi_routers'** of the model **'fastapi.endpoint'** you are inheriting from into your new addon. -.. code-block:: python +.. code:: python - class FastapiEndpoint(models.Model): + class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" + _inherit = "fastapi.endpoint" - def _get_fastapi_routers(self) -> List[APIRouter]: - routers = super()._get_fastapi_routers() - if self.app == "demo": - routers.append(additional_demo_api_router) - return routers + def _get_fastapi_routers(self) -> List[APIRouter]: + routers = super()._get_fastapi_routers() + if self.app == "demo": + routers.append(additional_demo_api_router) + return routers - additional_demo_api_router = APIRouter() + additional_demo_api_router = APIRouter() - @additional_demo_api_router.get( - "/echo2", - response_model=EchoResponse, - dependencies=[Depends(odoo_env)], - ) - async def echo2( - message: str, - odoo_env: Annotated[Environment, Depends(odoo_env)], - ) -> EchoResponse: - """Echo the message""" - echo = odoo_env["demo.fastapi.endpoint"].echo2(message) - return EchoResponse(message=f"Echo2: {echo}") + @additional_demo_api_router.get( + "/echo2", + response_model=EchoResponse, + dependencies=[Depends(odoo_env)], + ) + async def echo2( + message: str, + odoo_env: Annotated[Environment, Depends(odoo_env)], + ) -> EchoResponse: + """Echo the message""" + echo = odoo_env["demo.fastapi.endpoint"].echo2(message) + return EchoResponse(message=f"Echo2: {echo}") - -In this way, the new router is added to the list of routers of your app only if -the app is called for a database where your new addon is installed. +In this way, the new router is added to the list of routers of your app +only if the app is called for a database where your new addon is +installed. Extending the model used as parameter or as response of the route handler -========================================================================= - -The fastapi python library uses the pydantic library to define the models. By -default, once a model is defined, it's not possible to extend it. However, a -companion python library called -`extendable_pydantic `_ provides -a way to use inheritance with pydantic models to extend an existing model. If -used alone, it's your responsibility to instruct this library the list of -extensions to apply to a model and the order to apply them. This is not very -convenient. Fortunately, an dedicated odoo addon exists to make this process -complete transparent. This addon is called -`odoo-addon-extendable-fastapi `_. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The fastapi python library uses the pydantic library to define the +models. By default, once a model is defined, it's not possible to extend +it. However, a companion python library called +`extendable_pydantic `__ +provides a way to use inheritance with pydantic models to extend an +existing model. If used alone, it's your responsibility to instruct this +library the list of extensions to apply to a model and the order to +apply them. This is not very convenient. Fortunately, an dedicated odoo +addon exists to make this process complete transparent. This addon is +called +`odoo-addon-extendable-fastapi `__. When you want to allow other addons to extend a pydantic model, you must -first define the model as an extendable model by using a dedicated metaclass - -.. code-block:: python +first define the model as an extendable model by using a dedicated +metaclass - from pydantic import BaseModel - from extendable_pydantic import ExtendableModelMeta +.. code:: python - class Partner(BaseModel, metaclass=ExtendableModelMeta): - name = 0.1 - model_config = ConfigDict(from_attributes=True) + from pydantic import BaseModel + from extendable_pydantic import ExtendableModelMeta -As any other pydantic model, you can now use this model as parameter or as response -of a route handler. You can also use all the features of models defined with -pydantic. + class Partner(BaseModel, metaclass=ExtendableModelMeta): + name = 0.1 + model_config = ConfigDict(from_attributes=True) -.. code-block:: python +As any other pydantic model, you can now use this model as parameter or +as response of a route handler. You can also use all the features of +models defined with pydantic. - @demo_api_router.get( - "/partner", - response_model=Location, - dependencies=[Depends(authenticated_partner)], - ) - async def partner( - partner: Annotated[ResPartner, Depends(authenticated_partner)], - ) -> Partner: - """Return the location""" - return Partner.model_validate(partner) +.. code:: python + @demo_api_router.get( + "/partner", + response_model=Location, + dependencies=[Depends(authenticated_partner)], + ) + async def partner( + partner: Annotated[ResPartner, Depends(authenticated_partner)], + ) -> Partner: + """Return the location""" + return Partner.model_validate(partner) -If you need to add a new field into the model **'Partner'**, you can extend it -in your new addon by defining a new model that inherits from the model **'Partner'**. +If you need to add a new field into the model **'Partner'**, you can +extend it in your new addon by defining a new model that inherits from +the model **'Partner'**. -.. code-block:: python +.. code:: python - from typing import Optional - from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner + from typing import Optional + from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner - class PartnerExtended(Partner, extends=Partner): - email: Optional[str] + class PartnerExtended(Partner, extends=Partner): + email: Optional[str] -If your new addon is installed in a database, a call to the route handler -**'/demo/partner'** will return a response with the new field **'email'** if a -value is provided by the odoo record. +If your new addon is installed in a database, a call to the route +handler **'/demo/partner'** will return a response with the new field +**'email'** if a value is provided by the odoo record. -.. code-block:: python +.. code:: python - { - "name": "John Doe", - "email": "jhon.doe@acsone.eu" - } + { + "name": "John Doe", + "email": "jhon.doe@acsone.eu" + } -If your new addon is not installed in a database, a call to the route handler -**'/demo/partner'** will only return the name of the partner. +If your new addon is not installed in a database, a call to the route +handler **'/demo/partner'** will only return the name of the partner. -.. code-block:: python +.. code:: python - { - "name": "John Doe" - } + { + "name": "John Doe" + } -.. note:: +Note - The liskov substitution principle has also to be respected. That means that - if you extend a model, you must add new required fields or you must provide - default values for the new optional fields. +The liskov substitution principle has also to be respected. That means +that if you extend a model, you must add new required fields or you must +provide default values for the new optional fields. Managing security into the route handlers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default the route handlers are processed using the user configured on the -**'fastapi.endpoint'** model instance. (default is the Public user). -You have seen previously how to define a dependency that will be used to enforce -the authentication of a partner. When a method depends on this dependency, the -'authenticated_partner_id' key is added to the context of the partner environment. -(If you don't need the partner as dependency but need to get an environment -with the authenticated user, you can use the dependency 'authenticated_partner_env' instead of +----------------------------------------- + +By default the route handlers are processed using the user configured on +the **'fastapi.endpoint'** model instance. (default is the Public user). +You have seen previously how to define a dependency that will be used to +enforce the authentication of a partner. When a method depends on this +dependency, the 'authenticated_partner_id' key is added to the context +of the partner environment. (If you don't need the partner as dependency +but need to get an environment with the authenticated user, you can use +the dependency 'authenticated_partner_env' instead of 'authenticated_partner'.) -The fastapi addon extends the 'ir.rule' model to add into the evaluation context -of the security rules the key 'authenticated_partner_id' that contains the id -of the authenticated partner. - -As briefly introduced in a previous section, a good practice when you develop a -fastapi app and you want to protect your data in an efficient and traceable way is to: - -* create a new user specific to the app but with any access rights. -* create a security group specific to the app and add the user to this group. (This - group must implies the group 'AFastAPI Endpoint Runner' that give the - minimal access rights) -* for each model you want to protect: - - * add a 'ir.model.access' record for the model to allow read access to your model - and add the group to the record. - * create a new 'ir.rule' record for the model that restricts the access to the - records of the model to the authenticated partner by using the key - 'authenticated_partner_id' in domain of the rule. (or to the user defined on - the 'fastapi.endpoint' model instance if the method is public) - -* add a dependency on the 'authenticated_partner' to your handlers when you need - to access the authenticated partner or ensure that the service is called by an - authenticated partner. - -.. code-block:: xml - - - My Demo Endpoint User - my_demo_app_user - - - - - My Demo Endpoint Group - - - - - - - My Demo App: access to sale.order - - - - - - - - - - - Sale Order Rule - - [('partner_id', '=', authenticated_partner_id)] - - +The fastapi addon extends the 'ir.rule' model to add into the evaluation +context of the security rules the key 'authenticated_partner_id' that +contains the id of the authenticated partner. + +As briefly introduced in a previous section, a good practice when you +develop a fastapi app and you want to protect your data in an efficient +and traceable way is to: + +- create a new user specific to the app but with any access rights. +- create a security group specific to the app and add the user to this + group. (This group must implies the group 'AFastAPI Endpoint Runner' + that give the minimal access rights) +- for each model you want to protect: + + - add a 'ir.model.access' record for the model to allow read access + to your model and add the group to the record. + - create a new 'ir.rule' record for the model that restricts the + access to the records of the model to the authenticated partner by + using the key 'authenticated_partner_id' in domain of the rule. + (or to the user defined on the 'fastapi.endpoint' model instance + if the method is public) + +- add a dependency on the 'authenticated_partner' to your handlers when + you need to access the authenticated partner or ensure that the + service is called by an authenticated partner. + +.. code:: xml + + + My Demo Endpoint User + my_demo_app_user + + + + + My Demo Endpoint Group + + + + + + + My Demo App: access to sale.order + + + + + + + + + + + Sale Order Rule + + [('partner_id', '=', authenticated_partner_id)] + + How to test your fastapi app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Thanks to the starlette test client, it's possible to test your fastapi app -in a very simple way. With the test client, you can call your route handlers -as if they were real http endpoints. The test client is available in the -**'fastapi.testclient'** module. - -Once again the dependency injection mechanism comes to the rescue by allowing -you to inject into the test client specific implementations of the dependencies -normally provided by the normal processing of the request by the fastapi app. -(for example, you can inject a mock of the dependency 'authenticated_partner' -to test the behavior of your route handlers when the partner is not authenticated, -you can also inject a mock for the odoo_env etc...) - -The fastapi addon provides a base class for the test cases that you can use to -write your tests. This base class is **'odoo.fastapi.tests.common.FastAPITransactionCase'**. -This class mainly provides the method **'_create_test_client'** that you can -use to create a test client for your fastapi app. This method encapsulates the -creation of the test client and the injection of the dependencies. It also -ensures that the odoo environment is make available into the context of the -route handlers. This method is designed to be used when you need to test your -app or when you need to test a specific router (It's therefore easy to defines -tests for routers in an addon that doesn't provide a fastapi endpoint). - -With this base class, writing a test for a route handler is as simple as: - -.. code-block:: python - - from odoo.fastapi.tests.common import FastAPITransactionCase - - from odoo.addons.fastapi import dependencies - from odoo.addons.fastapi.routers import demo_router - - class FastAPIDemoCase(FastAPITransactionCase): - - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") - cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) - - def test_hello_world(self) -> None: - with self._create_test_client(router=demo_router) as test_client: - response: Response = test_client.get("/demo/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(response.json(), {"Hello": "World"}) - - -In the previous example, we created a test client for the demo_router. We could -have created a test client for the whole app by not specifying the router but -the app instead. - -.. code-block:: python - - from odoo.fastapi.tests.common import FastAPITransactionCase - - from odoo.addons.fastapi import dependencies - from odoo.addons.fastapi.routers import demo_router - - class FastAPIDemoCase(FastAPITransactionCase): - - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") - cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) - - def test_hello_world(self) -> None: - demo_endpoint = self.env.ref("fastapi.fastapi_endpoint_demo") - with self._create_test_client(app=demo_endpoint._get_app()) as test_client: - response: Response = test_client.get(f"{demo_endpoint.root_path}/demo/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(response.json(), {"Hello": "World"}) - +---------------------------- + +Thanks to the starlette test client, it's possible to test your fastapi +app in a very simple way. With the test client, you can call your route +handlers as if they were real http endpoints. The test client is +available in the **'fastapi.testclient'** module. + +Once again the dependency injection mechanism comes to the rescue by +allowing you to inject into the test client specific implementations of +the dependencies normally provided by the normal processing of the +request by the fastapi app. (for example, you can inject a mock of the +dependency 'authenticated_partner' to test the behavior of your route +handlers when the partner is not authenticated, you can also inject a +mock for the odoo_env etc...) + +The fastapi addon provides a base class for the test cases that you can +use to write your tests. This base class is +**'odoo.fastapi.tests.common.FastAPITransactionCase'**. This class +mainly provides the method **'\_create_test_client'** that you can use +to create a test client for your fastapi app. This method encapsulates +the creation of the test client and the injection of the dependencies. +It also ensures that the odoo environment is make available into the +context of the route handlers. This method is designed to be used when +you need to test your app or when you need to test a specific router +(It's therefore easy to defines tests for routers in an addon that +doesn't provide a fastapi endpoint). + +With this base class, writing a test for a route handler is as simple +as: + +.. code:: python + + from odoo.fastapi.tests.common import FastAPITransactionCase + + from odoo.addons.fastapi import dependencies + from odoo.addons.fastapi.routers import demo_router + + class FastAPIDemoCase(FastAPITransactionCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") + cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) + + def test_hello_world(self) -> None: + with self._create_test_client(router=demo_router) as test_client: + response: Response = test_client.get("/demo/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(response.json(), {"Hello": "World"}) + +In the previous example, we created a test client for the demo_router. +We could have created a test client for the whole app by not specifying +the router but the app instead. + +.. code:: python + + from odoo.fastapi.tests.common import FastAPITransactionCase + + from odoo.addons.fastapi import dependencies + from odoo.addons.fastapi.routers import demo_router + + class FastAPIDemoCase(FastAPITransactionCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") + cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) + + def test_hello_world(self) -> None: + demo_endpoint = self.env.ref("fastapi.fastapi_endpoint_demo") + with self._create_test_client(app=demo_endpoint._get_app()) as test_client: + response: Response = test_client.get(f"{demo_endpoint.root_path}/demo/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(response.json(), {"Hello": "World"}) Overall considerations when you develop an fastapi app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Developing a fastapi app requires to follow some good practices to ensure that -the app is robust and easy to maintain. Here are some of them: - -* A route handler must be as simple as possible. It must not contain any - business logic. The business logic must be implemented into the service - layer. The route handler must only call the service layer and return the - result of the service layer. To ease extension on your business logic, your - service layer can be implemented as an odoo abstract model that can be - inherited by other addons. - -* A route handler should not expose the internal data structure and api of Odoo. - It should provide the api that is needed by the client. More widely, an app - provides a set of services that address a set of use cases specific to - a well defined functional domain. You must always keep in mind that your api - will remain the same for a long time even if you upgrade your odoo version - of modify your business logic. - -* A route handler is a transactional unit of work. When you design your api - you must ensure that the completeness of a use case is guaranteed by a single - transaction. If you need to perform several transactions to complete a use - case, you introduce a risk of inconsistency in your data or extra complexity - in your client code. - -* Properly handle the errors. The route handler must return a proper error - response when an error occurs. The error response must be consistent with - the rest of the api. The error response must be documented in the api - documentation. By default, the **'odoo-addon-fastapi'** module handles - the common exception types defined in the **'odoo.exceptions'** module - and returns a proper error response with the corresponding http status code. - An error in the route handler must always return an error response with a - http status code different from 200. The error response must contain a - human readable message that can be displayed to the user. The error response - can also contain a machine readable code that can be used by the client to - handle the error in a specific way. - -* When you design your json document through the pydantic models, you must - use the appropriate data types. For example, you must use the data type - **'datetime.date'** to represent a date and not a string. You must also - properly define the constraints on the fields. For example, if a field - is optional, you must use the data type **'typing.Optional'**. - `pydantic`_ provides everything you need to - properly define your json document. - -* Always use an appropriate pydantic model as request and/or response for - your route handler. Constraints on the fields of the pydantic model must - apply to the specific use case. For example, if your route handler is used - to create a sale order, the pydantic model must not contain the field - 'id' because the id of the sale order will be generated by the route handler. - But if the id is required afterwords, the pydantic model for the response - must contain the field 'id' as required. - -* Uses descriptive property names in your json documents. For example, avoid the - use of documents providing a flat list of key value pairs. - -* Be consistent in the naming of your fields into your json documents. For example, - if you use 'id' to represent the id of a sale order, you must use 'id' to represent - the id of all the other objects. - -* Be consistent in the naming style of your fields. Always prefer underscore - to camel case. - -* Always use plural for the name of the fields that contain a list of items. - For example, if you have a field 'lines' that contains a list of sale order - lines, you must use 'lines' and not 'line'. - -* You can't expect that a client will provide you the identifier of a specific - record in odoo (for example the id of a carrier) if you don't provide a - specific route handler to retrieve the list of available records. Sometimes, - the client must share with odoo the identity of a specific record to be - able to perform an appropriate action specific to this record (for example, - the processing of a payment is different for each payment acquirer). In this - case, you must provide a specific attribute that allows both the client and - odoo to identify the record. The field 'provider' on a payment acquirer allows - you to identify a specific record in odoo. This kind of approach - allows both the client and odoo to identify the record without having to rely - on the id of the record. (This will ensure that the client will not break - if the id of the record is changed in odoo for example when tests are run - on an other database). - -* Always use the same name for the same kind of object. For example, if you - have a field 'lines' that contains a list of sale order lines, you must - use the same name for the same kind of object in all the other json documents. - -* Manage relations between objects in your json documents the same way. - By default, you should return the id of the related object in the json document. - But this is not always possible or convenient, so you can also return the - related object in the json document. The main advantage of returning the id - of the related object is that it allows you to avoid the `n+1 problem - `_ . The - main advantage of returning the related object in the json document is that - it allows you to avoid an extra call to retrieve the related object. - By keeping in mind the pros and cons of each approach, you can choose the - best one for your use case. Once it's done, you must be consistent in the - way you manage the relations of the same object. - -* It's not always a good idea to name your fields into your json documents - with the same name as the fields of the corresponding odoo model. For example, - in your document representing a sale order, you must not use the name 'order_line' - for the field that contains the list of sale order lines. The name 'order_line' - in addition to being confusing and not consistent with the best practices, is - not auto-descriptive. The name 'lines' is much better. - -* Keep a defensive programming approach. If you provide a route handler that - returns a list of records, you must ensure that the computation of the list - is not too long or will not drain your server resources. For example, - for search route handlers, you must ensure that the search is limited to - a reasonable number of records by default. - -* As a corollary of the previous point, a search handler must always use the - pagination mechanism with a reasonable default page size. The result list - must be enclosed in a json document that contains the count of records into - the system matching your search criteria and the list of records for the given - page and size. - -* Use plural for the name of a service. For example, if you provide a service - that allows you to manage the sale orders, you must use the name 'sale_orders' - and not 'sale_order'. - - - -* ... and many more. - -We could write a book about the best practices to follow when you design your api -but we will stop here. This list is the result of our experience at `ACSONE SA/NV -`_ and it evolves over time. It's a kind of rescue kit that we -would provide to a new developer that starts to design an api. This kit must -be accompanied with the reading of some useful resources link like the `REST Guidelines -`_. On a technical level, -the `fastapi documentation `_ provides a lot of -useful information as well, with a lot of examples. Last but not least, the -`pydantic`_ documentation is also very useful. +------------------------------------------------------ + +Developing a fastapi app requires to follow some good practices to +ensure that the app is robust and easy to maintain. Here are some of +them: + +- A route handler must be as simple as possible. It must not contain + any business logic. The business logic must be implemented into the + service layer. The route handler must only call the service layer and + return the result of the service layer. To ease extension on your + business logic, your service layer can be implemented as an odoo + abstract model that can be inherited by other addons. +- A route handler should not expose the internal data structure and api + of Odoo. It should provide the api that is needed by the client. More + widely, an app provides a set of services that address a set of use + cases specific to a well defined functional domain. You must always + keep in mind that your api will remain the same for a long time even + if you upgrade your odoo version of modify your business logic. +- A route handler is a transactional unit of work. When you design your + api you must ensure that the completeness of a use case is guaranteed + by a single transaction. If you need to perform several transactions + to complete a use case, you introduce a risk of inconsistency in your + data or extra complexity in your client code. +- Properly handle the errors. The route handler must return a proper + error response when an error occurs. The error response must be + consistent with the rest of the api. The error response must be + documented in the api documentation. By default, the + **'odoo-addon-fastapi'** module handles the common exception types + defined in the **'odoo.exceptions'** module and returns a proper + error response with the corresponding http status code. An error in + the route handler must always return an error response with a http + status code different from 200. The error response must contain a + human readable message that can be displayed to the user. The error + response can also contain a machine readable code that can be used by + the client to handle the error in a specific way. +- When you design your json document through the pydantic models, you + must use the appropriate data types. For example, you must use the + data type **'datetime.date'** to represent a date and not a string. + You must also properly define the constraints on the fields. For + example, if a field is optional, you must use the data type + **'typing.Optional'**. `pydantic `__ + provides everything you need to properly define your json document. +- Always use an appropriate pydantic model as request and/or response + for your route handler. Constraints on the fields of the pydantic + model must apply to the specific use case. For example, if your route + handler is used to create a sale order, the pydantic model must not + contain the field 'id' because the id of the sale order will be + generated by the route handler. But if the id is required afterwords, + the pydantic model for the response must contain the field 'id' as + required. +- Uses descriptive property names in your json documents. For example, + avoid the use of documents providing a flat list of key value pairs. +- Be consistent in the naming of your fields into your json documents. + For example, if you use 'id' to represent the id of a sale order, you + must use 'id' to represent the id of all the other objects. +- Be consistent in the naming style of your fields. Always prefer + underscore to camel case. +- Always use plural for the name of the fields that contain a list of + items. For example, if you have a field 'lines' that contains a list + of sale order lines, you must use 'lines' and not 'line'. +- You can't expect that a client will provide you the identifier of a + specific record in odoo (for example the id of a carrier) if you + don't provide a specific route handler to retrieve the list of + available records. Sometimes, the client must share with odoo the + identity of a specific record to be able to perform an appropriate + action specific to this record (for example, the processing of a + payment is different for each payment acquirer). In this case, you + must provide a specific attribute that allows both the client and + odoo to identify the record. The field 'provider' on a payment + acquirer allows you to identify a specific record in odoo. This kind + of approach allows both the client and odoo to identify the record + without having to rely on the id of the record. (This will ensure + that the client will not break if the id of the record is changed in + odoo for example when tests are run on an other database). +- Always use the same name for the same kind of object. For example, if + you have a field 'lines' that contains a list of sale order lines, + you must use the same name for the same kind of object in all the + other json documents. +- Manage relations between objects in your json documents the same way. + By default, you should return the id of the related object in the + json document. But this is not always possible or convenient, so you + can also return the related object in the json document. The main + advantage of returning the id of the related object is that it allows + you to avoid the `n+1 + problem `__ . The main + advantage of returning the related object in the json document is + that it allows you to avoid an extra call to retrieve the related + object. By keeping in mind the pros and cons of each approach, you + can choose the best one for your use case. Once it's done, you must + be consistent in the way you manage the relations of the same object. +- It's not always a good idea to name your fields into your json + documents with the same name as the fields of the corresponding odoo + model. For example, in your document representing a sale order, you + must not use the name 'order_line' for the field that contains the + list of sale order lines. The name 'order_line' in addition to being + confusing and not consistent with the best practices, is not + auto-descriptive. The name 'lines' is much better. +- Keep a defensive programming approach. If you provide a route handler + that returns a list of records, you must ensure that the computation + of the list is not too long or will not drain your server resources. + For example, for search route handlers, you must ensure that the + search is limited to a reasonable number of records by default. +- As a corollary of the previous point, a search handler must always + use the pagination mechanism with a reasonable default page size. The + result list must be enclosed in a json document that contains the + count of records into the system matching your search criteria and + the list of records for the given page and size. +- Use plural for the name of a service. For example, if you provide a + service that allows you to manage the sale orders, you must use the + name 'sale_orders' and not 'sale_order'. +- ... and many more. + +We could write a book about the best practices to follow when you design +your api but we will stop here. This list is the result of our +experience at `ACSONE SA/NV `__ and it evolves over +time. It's a kind of rescue kit that we would provide to a new developer +that starts to design an api. This kit must be accompanied with the +reading of some useful resources link like the `REST +Guidelines `__. On +a technical level, the `fastapi +documentation `__ provides a lot of +useful information as well, with a lot of examples. Last but not least, +the `pydantic `__ documentation is also very +useful. Miscellaneous -~~~~~~~~~~~~~ +------------- Development of a search route handler -===================================== - -The **'odoo-addon-fastapi'** module provides 2 useful piece of code to help -you be consistent when writing a route handler for a search route. - -1. A dependency method to use to specify the pagination parameters in the same - way for all the search route handlers: **'odoo.addons.fastapi.paging'**. -2. A PagedCollection pydantic model to use to return the result of a search route - handler enclosed in a json document that contains the count of records. - -.. code-block:: python - - from typing import Annotated - from pydantic import BaseModel - - from odoo.api import Environment - from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env - from odoo.addons.fastapi.schemas import PagedCollection, Paging - - class SaleOrder(BaseModel): - id: int - name: str - model_config = ConfigDict(from_attributes=True) - - - @router.get( - "/sale_orders", - response_model=PagedCollection[SaleOrder], - response_model_exclude_unset=True, - ) - def get_sale_orders( - paging: Annotated[Paging, Depends(paging)], - env: Annotated[Environment, Depends(authenticated_partner_env)], - ) -> PagedCollection[SaleOrder]: - """Get the list of sale orders.""" - count = env["sale.order"].search_count([]) - orders = env["sale.order"].search([], limit=paging.limit, offset=paging.offset) - return PagedCollection[SaleOrder]( - count=count, - items=[SaleOrder.model_validate(order) for order in orders], - ) - -.. note:: - - The **'odoo.addons.fastapi.schemas.Paging'** and **'odoo.addons.fastapi.schemas.PagedCollection'** - pydantic models are not designed to be extended to not introduce a - dependency between the **'odoo-addon-fastapi'** module and the **'odoo-addon-extendable'** - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The **'odoo-addon-fastapi'** module provides 2 useful piece of code to +help you be consistent when writing a route handler for a search route. + +1. A dependency method to use to specify the pagination parameters in + the same way for all the search route handlers: + **'odoo.addons.fastapi.paging'**. +2. A PagedCollection pydantic model to use to return the result of a + search route handler enclosed in a json document that contains the + count of records. + +.. code:: python + + from typing import Annotated + from pydantic import BaseModel + + from odoo.api import Environment + from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env + from odoo.addons.fastapi.schemas import PagedCollection, Paging + + class SaleOrder(BaseModel): + id: int + name: str + model_config = ConfigDict(from_attributes=True) + + + @router.get( + "/sale_orders", + response_model=PagedCollection[SaleOrder], + response_model_exclude_unset=True, + ) + def get_sale_orders( + paging: Annotated[Paging, Depends(paging)], + env: Annotated[Environment, Depends(authenticated_partner_env)], + ) -> PagedCollection[SaleOrder]: + """Get the list of sale orders.""" + count = env["sale.order"].search_count([]) + orders = env["sale.order"].search([], limit=paging.limit, offset=paging.offset) + return PagedCollection[SaleOrder]( + count=count, + items=[SaleOrder.model_validate(order) for order in orders], + ) + +Note + +The **'odoo.addons.fastapi.schemas.Paging'** and +**'odoo.addons.fastapi.schemas.PagedCollection'** pydantic models are +not designed to be extended to not introduce a dependency between the +**'odoo-addon-fastapi'** module and the **'odoo-addon-extendable'** Customization of the error handling -=================================== - -The error handling a very important topic in the design of the fastapi integration -with odoo. It must ensure that the error messages are properly return to the client -and that the transaction is properly roll backed. The **'fastapi'** module provides -a way to register custom error handlers. The **'odoo.addons.fastapi.error_handlers'** -module provides the default error handlers that are registered by default when -a new instance of the **'FastAPI'** class is created. When an app is initialized in -'fastapi.endpoint' model, the method `_get_app_exception_handlers` is called to -get a dictionary of error handlers. This method is designed to be overridden -in a custom module to provide custom error handlers. You can override the handler -for a specific exception class or you can add a new handler for a new exception -or even replace all the handlers by your own handlers. Whatever you do, you must -ensure that the transaction is properly roll backed. - -Some could argue that the error handling can't be extended since the error handlers -are global method not defined in an odoo model. Since the method providing the -the error handlers definitions is defined on the 'fastapi.endpoint' model, it's -not a problem at all, you just need to think another way to do it that by inheritance. - -A solution could be to develop you own error handler to be able to process the -error and chain the call to the default error handler. - -.. code-block:: python - - class MyCustomErrorHandler(): - def __init__(self, next_handler): - self.next_handler = next_handler - - def __call__(self, request: Request, exc: Exception) -> JSONResponse: - # do something with the error - response = self.next_handler(request, exc) - # do something with the response - return response - - -With this solution, you can now register your custom error handler by overriding -the method `_get_app_exception_handlers` in your custom module. - -.. code-block:: python - - class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" - - def _get_app_exception_handlers( - self, - ) -> Dict[ - Union[int, Type[Exception]], - Callable[[Request, Exception], Union[Response, Awaitable[Response]]], - ]: - handlers = super()._get_app_exception_handlers() - access_error_handler = handlers.get(odoo.exceptions.AccessError) - handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler) - return handlers - -In the previous example, we extend the error handler for the 'AccessError' exception -for all the endpoints. You can do the same for a specific app by checking the -'app' field of the 'fastapi.endpoint' record before registering your custom error -handler. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The error handling a very important topic in the design of the fastapi +integration with odoo. It must ensure that the error messages are +properly return to the client and that the transaction is properly roll +backed. The **'fastapi'** module provides a way to register custom error +handlers. The **'odoo.addons.fastapi.error_handlers'** module provides +the default error handlers that are registered by default when a new +instance of the **'FastAPI'** class is created. When an app is +initialized in 'fastapi.endpoint' model, the method +\_get_app_exception_handlers is called to get a dictionary of error +handlers. This method is designed to be overridden in a custom module to +provide custom error handlers. You can override the handler for a +specific exception class or you can add a new handler for a new +exception or even replace all the handlers by your own handlers. +Whatever you do, you must ensure that the transaction is properly roll +backed. + +Some could argue that the error handling can't be extended since the +error handlers are global method not defined in an odoo model. Since the +method providing the the error handlers definitions is defined on the +'fastapi.endpoint' model, it's not a problem at all, you just need to +think another way to do it that by inheritance. + +A solution could be to develop you own error handler to be able to +process the error and chain the call to the default error handler. + +.. code:: python + + class MyCustomErrorHandler(): + def __init__(self, next_handler): + self.next_handler = next_handler + + def __call__(self, request: Request, exc: Exception) -> JSONResponse: + # do something with the error + response = self.next_handler(request, exc) + # do something with the response + return response + +With this solution, you can now register your custom error handler by +overriding the method \_get_app_exception_handlers in your custom +module. + +.. code:: python + + class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + def _get_app_exception_handlers( + self, + ) -> Dict[ + Union[int, Type[Exception]], + Callable[[Request, Exception], Union[Response, Awaitable[Response]]], + ]: + handlers = super()._get_app_exception_handlers() + access_error_handler = handlers.get(odoo.exceptions.AccessError) + handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler) + return handlers + +In the previous example, we extend the error handler for the +'AccessError' exception for all the endpoints. You can do the same for a +specific app by checking the 'app' field of the 'fastapi.endpoint' +record before registering your custom error handler. FastAPI addons directory structure -================================== - -When you develop a new addon to expose an api with fastapi, it's a good practice -to follow the same directory structure and naming convention for the files -related to the api. It will help you to easily find the files related to the api -and it will help the other developers to understand your code. - -Here is the directory structure that we recommend. It's based on practices that -are used in the python community when developing a fastapi app. - -.. code-block:: - - . - ├── x_api - │   ├── data - │   │ ├── ... .xml - │   ├── demo - │   │ ├── ... .xml - │   ├── i18n - │   │ ├── ... .po - │   ├── models - │   │ ├── __init__.py - │   │ ├── fastapi_endpoint.py # your app - │   │ └── ... .py - │   └── routers - │   │ ├── __init__.py - │   │ ├── items.py - │   │ └── ... .py - │   ├── schemas | schemas.py - │   │ ├── __init__.py - │   │ ├── my_model.py # pydantic model - │   │ └── ... .py - │   ├── security - │   │ ├── ... .xml - │   ├── views - │   │ ├── ... .xml - │   ├── __init__.py - │   ├── __manifest__.py - │   ├── dependencies.py # custom dependencies - │   ├── error_handlers.py # custom error handlers - - -* The **'models'** directory contains the odoo models. When you define a new - app, as for the others addons, you will add your new model inheriting from - the **'fastapi.endpoint'** model in this directory. -* The **'routers'** directory contains the fastapi routers. You will add your - new routers in this directory. Each route starting with the same prefix should - be grouped in the same file. For example, all the routes starting with - '/items' should be defined in the **'items.py'** file. The **'__init__.py'** - file in this directory is used to import all the routers defined in the - directory and create a global router that can be used in an app. For example, - in your **'items.py'** file, you will define a router like this: - - .. code-block:: python - - router = APIRouter(tags=["items"]) - - router.get("/items", response_model=List[Item]) - def list_items(): - pass - - In the **'__init__.py'** file, you will import the router and add it to the global - router or your addon. - - .. code-block:: python - - from fastapi import APIRouter - - from .items import router as items_router - - router = APIRouter() - router.include_router(items_router) - -* The **'schemas.py'** will be used to define the pydantic models. For complex - APIs with a lot of models, it will be better to create a **'schemas'** directory - and split the models in different files. The **'__init__.py'** file in this - directory will be used to import all the models defined in the directory. - For example, in your **'my_model.py'** - file, you will define a model like this: - - .. code-block:: python - - from pydantic import BaseModel - - class MyModel(BaseModel): - name: str - description: str = None - - In the **'__init__.py'** file, you will import the model's classes from the - files in the directory. - - .. code-block:: python - - from .my_model import MyModel - - This will allow to always import the models from the schemas module whatever - the models are spread across different files or defined in the **'schemas.py'** - file. - - .. code-block:: python - - from x_api_addon.schemas import MyModel - -* The **'dependencies.py'** file contains the custom dependencies that you - will use in your routers. For example, you can define a dependency to - check the access rights of the user. -* The **'error_handlers.py'** file contains the custom error handlers that you - will use in your routers. The **'odoo-addon-fastapi'** module provides the - default error handlers for the common odoo exceptions. Chance are that you - will not need to define your own error handlers. But if you need to do it, - you can define them in this file. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -What's next? -~~~~~~~~~~~~ +When you develop a new addon to expose an api with fastapi, it's a good +practice to follow the same directory structure and naming convention +for the files related to the api. It will help you to easily find the +files related to the api and it will help the other developers to +understand your code. + +Here is the directory structure that we recommend. It's based on +practices that are used in the python community when developing a +fastapi app. + +:: + + . + ├── x_api + │   ├── data + │   │ ├── ... .xml + │   ├── demo + │   │ ├── ... .xml + │   ├── i18n + │   │ ├── ... .po + │   ├── models + │   │ ├── __init__.py + │   │ ├── fastapi_endpoint.py # your app + │   │ └── ... .py + │   └── routers + │   │ ├── __init__.py + │   │ ├── items.py + │   │ └── ... .py + │   ├── schemas | schemas.py + │   │ ├── __init__.py + │   │ ├── my_model.py # pydantic model + │   │ └── ... .py + │   ├── security + │   │ ├── ... .xml + │   ├── views + │   │ ├── ... .xml + │   ├── __init__.py + │   ├── __manifest__.py + │   ├── dependencies.py # custom dependencies + │   ├── error_handlers.py # custom error handlers + +- The **'models'** directory contains the odoo models. When you define + a new app, as for the others addons, you will add your new model + inheriting from the **'fastapi.endpoint'** model in this directory. + +- The **'routers'** directory contains the fastapi routers. You will + add your new routers in this directory. Each route starting with the + same prefix should be grouped in the same file. For example, all the + routes starting with '/items' should be defined in the **'items.py'** + file. The **'\__init\_\_.py'** file in this directory is used to + import all the routers defined in the directory and create a global + router that can be used in an app. For example, in your + **'items.py'** file, you will define a router like this: + + .. code:: python + + router = APIRouter(tags=["items"]) + + router.get("/items", response_model=List[Item]) + def list_items(): + pass + + In the **'\__init\_\_.py'** file, you will import the router and add + it to the global router or your addon. + + .. code:: python + + from fastapi import APIRouter + + from .items import router as items_router + + router = APIRouter() + router.include_router(items_router) + +- The **'schemas.py'** will be used to define the pydantic models. For + complex APIs with a lot of models, it will be better to create a + **'schemas'** directory and split the models in different files. The + **'\__init\_\_.py'** file in this directory will be used to import + all the models defined in the directory. For example, in your + **'my_model.py'** file, you will define a model like this: + + .. code:: python + + from pydantic import BaseModel + + class MyModel(BaseModel): + name: str + description: str = None + + In the **'\__init\_\_.py'** file, you will import the model's classes + from the files in the directory. + + .. code:: python + + from .my_model import MyModel + + This will allow to always import the models from the schemas module + whatever the models are spread across different files or defined in + the **'schemas.py'** file. + + .. code:: python + + from x_api_addon.schemas import MyModel + +- The **'dependencies.py'** file contains the custom dependencies that + you will use in your routers. For example, you can define a + dependency to check the access rights of the user. + +- The **'error_handlers.py'** file contains the custom error handlers + that you will use in your routers. The **'odoo-addon-fastapi'** + module provides the default error handlers for the common odoo + exceptions. Chance are that you will not need to define your own + error handlers. But if you need to do it, you can define them in this + file. -The **'odoo-addon-fastapi'** module is still in its early stage of development. -It will evolve over time to integrate your feedback and to provide the missing -features. It's now up to you to try it and to provide your feedback. +What's next? +------------ -.. _pydantic: https://docs.pydantic.dev/ +The **'odoo-addon-fastapi'** module is still in its early stage of +development. It will evolve over time to integrate your feedback and to +provide the missing features. It's now up to you to try it and to +provide your feedback. Known issues / Roadmap ====================== -The `roadmap `_ -and `known issues `_ can -be found on GitHub. - -The **FastAPI** module provides an easy way to use WebSockets. Unfortunately, this -support is not 'yet' available in the **Odoo** framework. The challenge is high -because the integration of the fastapi is based on the use of a specific middleware -that convert the WSGI request consumed by odoo to a ASGI request. The question -is to know if it is also possible to develop the same kind of bridge for the +The +`roadmap `__ +and `known +issues `__ +can be found on GitHub. + +The **FastAPI** module provides an easy way to use WebSockets. +Unfortunately, this support is not 'yet' available in the **Odoo** +framework. The challenge is high because the integration of the fastapi +is based on the use of a specific middleware that convert the WSGI +request consumed by odoo to a ASGI request. The question is to know if +it is also possible to develop the same kind of bridge for the WebSockets and to stream large responses. Changelog @@ -1481,54 +1528,60 @@ Changelog 16.0.1.2.3 (2023-12-21) -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- **Bugfixes** -- In case of exception in endpoint execution, close the database cursor after rollback. - - This is to ensure that the *retrying* method in *service/model.py* does not try - to flush data to the database. (`#405 `_) +- In case of exception in endpoint execution, close the database cursor + after rollback. + This is to ensure that the *retrying* method in *service/model.py* + does not try to flush data to the database. + (`#405 `__) 16.0.1.2.2 (2023-12-12) -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- **Bugfixes** -- When using the 'FastAPITransactionCase' class, allows to specify a specific - override of the 'authenticated_partner_impl' method into the list of - overrides to apply. Before this change, the 'authenticated_partner_impl' - override given in the 'overrides' parameter was always overridden in the - '_create_test_client' method of the 'FastAPITransactionCase' class. It's now - only overridden if the 'authenticated_partner_impl' method is not already - present in the list of overrides to apply and no specific partner is given. - If a specific partner is given at same time of an override for the - 'authenticated_partner_impl' method, an error is raised. (`#396 `_) - +- When using the 'FastAPITransactionCase' class, allows to specify a + specific override of the 'authenticated_partner_impl' method into the + list of overrides to apply. Before this change, the + 'authenticated_partner_impl' override given in the 'overrides' + parameter was always overridden in the '\_create_test_client' method + of the 'FastAPITransactionCase' class. It's now only overridden if + the 'authenticated_partner_impl' method is not already present in the + list of overrides to apply and no specific partner is given. If a + specific partner is given at same time of an override for the + 'authenticated_partner_impl' method, an error is raised. + (`#396 `__) 16.0.1.2.1 (2023-11-03) -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- **Bugfixes** -- Fix a typo in the Field declaration of the 'count' attribute of the 'PagedCollection' schema. - - Misspelt parameter was triggering a deprecation warning due to recent versions of Pydantic seeing it as an arbitrary parameter. (`#389 `_) +- Fix a typo in the Field declaration of the 'count' attribute of the + 'PagedCollection' schema. + Misspelt parameter was triggering a deprecation warning due to recent + versions of Pydantic seeing it as an arbitrary parameter. + (`#389 `__) 16.0.1.2.0 (2023-10-13) -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- **Features** -- The field *total* in the *PagedCollection* schema is replaced by the field *count*. - The field *total* is now deprecated and will be removed in the next major version. - This change is backward compatible. The json document returned will now - contain both fields *total* and *count* with the same value. In your python - code the field *total*, if used, will fill the field *count* with the same - value. You are encouraged to use the field *count* instead of *total* and adapt - your code accordingly. (`#380 `_) +- The field *total* in the *PagedCollection* schema is replaced by the + field *count*. The field *total* is now deprecated and will be + removed in the next major version. This change is backward + compatible. The json document returned will now contain both fields + *total* and *count* with the same value. In your python code the + field *total*, if used, will fill the field *count* with the same + value. You are encouraged to use the field *count* instead of *total* + and adapt your code accordingly. + (`#380 `__) Bug Tracker =========== @@ -1536,7 +1589,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -1544,17 +1597,17 @@ Credits ======= Authors -~~~~~~~ +------- * ACSONE SA/NV Contributors -~~~~~~~~~~~~ +------------ -* Laurent Mignon +- Laurent Mignon Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -1574,6 +1627,6 @@ Current `maintainer `__: |maintainer-lmignon| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 57ba1144..3f330344 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.2.5", + "version": "17.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], @@ -30,5 +30,5 @@ ] }, "development_status": "Beta", - "installable": False, + # "installable": False, } diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 018ff923..cc0af89d 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -2,9 +2,10 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). import logging +from collections.abc import Awaitable, Callable from functools import partial from itertools import chain -from typing import Any, Awaitable, Callable, Dict, List, Tuple, Type, Union +from typing import Any from a2wsgi import ASGIMiddleware from starlette.middleware import Middleware @@ -20,7 +21,6 @@ class FastapiEndpoint(models.Model): - _name = "fastapi.endpoint" _inherit = "endpoint.route.sync.mixin" _description = "FastAPI Endpoint" @@ -111,7 +111,7 @@ def _registered_endpoint_rule_keys(self): return tuple(res) @api.model - def _routing_impacting_fields(self) -> Tuple[str]: + def _routing_impacting_fields(self) -> tuple[str]: """The list of fields requiring to refresh the mount point of the pp into odoo if modified""" return ("root_path",) @@ -134,11 +134,11 @@ def _handle_route_updates(self, vals): if refresh_fastapi_app: self._reset_app() if "user_id" in vals: - self.get_uid.clear_cache(self) + self.env.registry.clear_cache() return False @api.model - def _fastapi_app_fields(self) -> List[str]: + def _fastapi_app_fields(self) -> list[str]: """The list of fields requiring to refresh the fastapi app if modified""" return [] @@ -181,13 +181,13 @@ def _get_routing_info(self): # csrf ????? } - def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]): + def _endpoint_registry_route_unique_key(self, routing: dict[str, Any]): route = "|".join(routing["routes"]) path = route.replace(self.root_path, "") return f"{self._name}:{self.id}:{path}" def _reset_app(self): - self.get_app.clear_cache(self) + self.env.registry.clear_cache() @api.model @tools.ormcache("root_path") @@ -220,7 +220,7 @@ def _get_app(self) -> FastAPI: app.add_exception_handler(exception, handler) return app - def _get_app_dependencies_overrides(self) -> Dict[Callable, Callable]: + def _get_app_dependencies_overrides(self) -> dict[Callable, Callable]: return { dependencies.fastapi_endpoint_id: partial(lambda a: a, self.id), dependencies.company_id: partial(lambda a: a, self.company_id.id), @@ -228,9 +228,9 @@ def _get_app_dependencies_overrides(self) -> Dict[Callable, Callable]: def _get_app_exception_handlers( self, - ) -> Dict[ - Union[int, Type[Exception]], - Callable[[Request, Exception], Union[Response, Awaitable[Response]]], + ) -> dict[ + (int | type[Exception]), + Callable[[Request, Exception], (Response | Awaitable[Response])], ]: """Return a dict of exception handlers to register on the app @@ -255,10 +255,12 @@ def _get_app_exception_handlers( odoo.exceptions.UserError: error_handlers._odoo_user_error_handler, odoo.exceptions.AccessError: error_handlers._odoo_access_error_handler, odoo.exceptions.MissingError: error_handlers._odoo_missing_error_handler, - odoo.exceptions.ValidationError: error_handlers._odoo_validation_error_handler, + odoo.exceptions.ValidationError: ( + error_handlers._odoo_validation_error_handler + ), } - def _prepare_fastapi_app_params(self) -> Dict[str, Any]: + def _prepare_fastapi_app_params(self) -> dict[str, Any]: """Return the params to pass to the Fast API app constructor""" return { "title": self.name, @@ -267,17 +269,17 @@ def _prepare_fastapi_app_params(self) -> Dict[str, Any]: "dependencies": self._get_fastapi_app_dependencies(), } - def _get_fastapi_routers(self) -> List[APIRouter]: + def _get_fastapi_routers(self) -> list[APIRouter]: """Return the api routers to use for the instance. This method must be implemented when registering a new api type. """ return [] - def _get_fastapi_app_middlewares(self) -> List[Middleware]: + def _get_fastapi_app_middlewares(self) -> list[Middleware]: """Return the middlewares to use for the fastapi app.""" return [] - def _get_fastapi_app_dependencies(self) -> List[Depends]: + def _get_fastapi_app_dependencies(self) -> list[Depends]: """Return the dependencies to use for the fastapi app.""" return [Depends(dependencies.accept_language)] diff --git a/fastapi/models/fastapi_endpoint_demo.py b/fastapi/models/fastapi_endpoint_demo.py index 8ddbb95f..7011b32c 100644 --- a/fastapi/models/fastapi_endpoint_demo.py +++ b/fastapi/models/fastapi_endpoint_demo.py @@ -1,6 +1,6 @@ # Copyright 2022 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -from typing import Annotated, Any, List +from typing import Annotated, Any from odoo import _, api, fields, models from odoo.api import Environment @@ -20,7 +20,6 @@ class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" app: str = fields.Selection( @@ -31,7 +30,7 @@ class FastapiEndpoint(models.Model): string="Authenciation method", ) - def _get_fastapi_routers(self) -> List[APIRouter]: + def _get_fastapi_routers(self) -> list[APIRouter]: if self.app == "demo": return [demo_router] return super()._get_fastapi_routers() @@ -48,7 +47,7 @@ def _valdiate_demo_auth_method(self): ) @api.model - def _fastapi_app_fields(self) -> List[str]: + def _fastapi_app_fields(self) -> list[str]: fields = super()._fastapi_app_fields() fields.append("demo_auth_method") return fields diff --git a/fastapi/pyproject.toml b/fastapi/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/fastapi/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fastapi/readme/CONTRIBUTORS.md b/fastapi/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..f2af9193 --- /dev/null +++ b/fastapi/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Laurent Mignon \<\> diff --git a/fastapi/readme/CONTRIBUTORS.rst b/fastapi/readme/CONTRIBUTORS.rst deleted file mode 100644 index 172b2d22..00000000 --- a/fastapi/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1 +0,0 @@ -* Laurent Mignon diff --git a/fastapi/readme/DESCRIPTION.md b/fastapi/readme/DESCRIPTION.md new file mode 100644 index 00000000..d0261086 --- /dev/null +++ b/fastapi/readme/DESCRIPTION.md @@ -0,0 +1,41 @@ +This addon provides the basis to smoothly integrate the +[FastAPI](https://fastapi.tiangolo.com/) framework into Odoo. + +This integration allows you to use all the goodies from +[FastAPI](https://fastapi.tiangolo.com/) to build custom APIs for your +Odoo server based on standard Python type hints. + +**What is building an API?** + +An API is a set of functions that can be called from the outside world. +The goal of an API is to provide a way to interact with your application +from the outside world without having to know how it works internally. A +common mistake when you are building an API is to expose all the +internal functions of your application and therefore create a tight +coupling between the outside world and your internal datamodel and +business logic. This is not a good idea because it makes it very hard to +change your internal datamodel and business logic without breaking the +outside world. + +When you are building an API, you define a contract between the outside +world and your application. This contract is defined by the functions +that you expose and the parameters that you accept. This contract is the +API. When you change your internal datamodel and business logic, you can +still keep the same API contract and therefore you don't break the +outside world. Even if you change your implementation, as long as you +keep the same API contract, the outside world will still work. This is +the beauty of an API and this is why it is so important to design a good +API. + +A good API is designed to be stable and to be easy to use. It's designed +to provide high-level functions related to a specific use case. It's +designed to be easy to use by hiding the complexity of the internal +datamodel and business logic. A common mistake when you are building an +API is to expose all the internal functions of your application and let +the oustide world deal with the complexity of your internal datamodel +and business logic. Don't forget that on a transactional point of view, +each call to an API function is a transaction. This means that if a +specific use case requires multiple calls to your API, you should +provide a single function that does all the work in a single +transaction. This why APIs methods are called high-level and atomic +functions. diff --git a/fastapi/readme/DESCRIPTION.rst b/fastapi/readme/DESCRIPTION.rst deleted file mode 100644 index c3939a2a..00000000 --- a/fastapi/readme/DESCRIPTION.rst +++ /dev/null @@ -1,38 +0,0 @@ -This addon provides the basis to smoothly integrate the `FastAPI`_ -framework into Odoo. - -This integration allows you to use all the goodies from `FastAPI`_ to build custom -APIs for your Odoo server based on standard Python type hints. - -**What is building an API?** - -An API is a set of functions that can be called from the outside world. The -goal of an API is to provide a way to interact with your application from the -outside world without having to know how it works internally. A common mistake -when you are building an API is to expose all the internal functions of your -application and therefore create a tight coupling between the outside world and -your internal datamodel and business logic. This is not a good idea because it -makes it very hard to change your internal datamodel and business logic without -breaking the outside world. - -When you are building an API, you define a contract between the outside world -and your application. This contract is defined by the functions that you expose -and the parameters that you accept. This contract is the API. When you change -your internal datamodel and business logic, you can still keep the same API -contract and therefore you don't break the outside world. Even if you change -your implementation, as long as you keep the same API contract, the outside -world will still work. This is the beauty of an API and this is why it is so -important to design a good API. - -A good API is designed to be stable and to be easy to use. It's designed to -provide high-level functions related to a specific use case. It's designed to -be easy to use by hiding the complexity of the internal datamodel and business -logic. A common mistake when you are building an API is to expose all the internal -functions of your application and let the oustide world deal with the complexity -of your internal datamodel and business logic. Don't forget that on a transactional -point of view, each call to an API function is a transaction. This means that -if a specific use case requires multiple calls to your API, you should provide -a single function that does all the work in a single transaction. This why APIs -methods are called high-level and atomic functions. - -.. _FastAPI: https://fastapi.tiangolo.com/ diff --git a/fastapi/readme/HISTORY.md b/fastapi/readme/HISTORY.md new file mode 100644 index 00000000..c28f9f81 --- /dev/null +++ b/fastapi/readme/HISTORY.md @@ -0,0 +1,61 @@ +## 16.0.1.2.5 (2024-01-17) + +**Bugfixes** + +- Odoo has done an update and now, it checks domains of ir.rule on creation and modification. + + The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context. + This field wasn't always set and this caused an error when Odoo checked the domain. + So now it is set to *False* by default. (`#410 `_) + +## 16.0.1.2.3 (2023-12-21) + +**Bugfixes** + +- In case of exception in endpoint execution, close the database cursor + after rollback. + + This is to ensure that the *retrying* method in *service/model.py* + does not try to flush data to the database. + ([\#405](https://github.com/OCA/rest-framework/issues/405)) + +## 16.0.1.2.2 (2023-12-12) + +**Bugfixes** + +- When using the 'FastAPITransactionCase' class, allows to specify a + specific override of the 'authenticated_partner_impl' method into the + list of overrides to apply. Before this change, the + 'authenticated_partner_impl' override given in the 'overrides' + parameter was always overridden in the '\_create_test_client' method + of the 'FastAPITransactionCase' class. It's now only overridden if the + 'authenticated_partner_impl' method is not already present in the list + of overrides to apply and no specific partner is given. If a specific + partner is given at same time of an override for the + 'authenticated_partner_impl' method, an error is raised. + ([\#396](https://github.com/OCA/rest-framework/issues/396)) + +## 16.0.1.2.1 (2023-11-03) + +**Bugfixes** + +- Fix a typo in the Field declaration of the 'count' attribute of the + 'PagedCollection' schema. + + Misspelt parameter was triggering a deprecation warning due to recent + versions of Pydantic seeing it as an arbitrary parameter. + ([\#389](https://github.com/OCA/rest-framework/issues/389)) + +## 16.0.1.2.0 (2023-10-13) + +**Features** + +- The field *total* in the *PagedCollection* schema is replaced by the + field *count*. The field *total* is now deprecated and will be removed + in the next major version. This change is backward compatible. The + json document returned will now contain both fields *total* and + *count* with the same value. In your python code the field *total*, if + used, will fill the field *count* with the same value. You are + encouraged to use the field *count* instead of *total* and adapt your + code accordingly. + ([\#380](https://github.com/OCA/rest-framework/issues/380)) diff --git a/fastapi/readme/HISTORY.rst b/fastapi/readme/HISTORY.rst deleted file mode 100644 index 54d0c7f1..00000000 --- a/fastapi/readme/HISTORY.rst +++ /dev/null @@ -1,61 +0,0 @@ -16.0.1.2.5 (2024-01-17) -~~~~~~~~~~~~~~~~~~~~~~~ - -**Bugfixes** - -- Odoo has done an update and now, it checks domains of ir.rule on creation and modification. - - The ir.rule 'Fastapi: Running user rule' uses a field (authenticate_partner_id) that comes from the context. - This field wasn't always set and this caused an error when Odoo checked the domain. - So now it is set to *False* by default. (`#410 `_) - - -16.0.1.2.3 (2023-12-21) -~~~~~~~~~~~~~~~~~~~~~~~ - -**Bugfixes** - -- In case of exception in endpoint execution, close the database cursor after rollback. - - This is to ensure that the *retrying* method in *service/model.py* does not try - to flush data to the database. (`#405 `_) - - -16.0.1.2.2 (2023-12-12) -~~~~~~~~~~~~~~~~~~~~~~~ - -**Bugfixes** - -- When using the 'FastAPITransactionCase' class, allows to specify a specific - override of the 'authenticated_partner_impl' method into the list of - overrides to apply. Before this change, the 'authenticated_partner_impl' - override given in the 'overrides' parameter was always overridden in the - '_create_test_client' method of the 'FastAPITransactionCase' class. It's now - only overridden if the 'authenticated_partner_impl' method is not already - present in the list of overrides to apply and no specific partner is given. - If a specific partner is given at same time of an override for the - 'authenticated_partner_impl' method, an error is raised. (`#396 `_) - - -16.0.1.2.1 (2023-11-03) -~~~~~~~~~~~~~~~~~~~~~~~ - -**Bugfixes** - -- Fix a typo in the Field declaration of the 'count' attribute of the 'PagedCollection' schema. - - Misspelt parameter was triggering a deprecation warning due to recent versions of Pydantic seeing it as an arbitrary parameter. (`#389 `_) - - -16.0.1.2.0 (2023-10-13) -~~~~~~~~~~~~~~~~~~~~~~~ - -**Features** - -- The field *total* in the *PagedCollection* schema is replaced by the field *count*. - The field *total* is now deprecated and will be removed in the next major version. - This change is backward compatible. The json document returned will now - contain both fields *total* and *count* with the same value. In your python - code the field *total*, if used, will fill the field *count* with the same - value. You are encouraged to use the field *count* instead of *total* and adapt - your code accordingly. (`#380 `_) diff --git a/fastapi/readme/ROADMAP.md b/fastapi/readme/ROADMAP.md new file mode 100644 index 00000000..4541abb7 --- /dev/null +++ b/fastapi/readme/ROADMAP.md @@ -0,0 +1,13 @@ +The +[roadmap](https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Afastapi) +and [known +issues](https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Afastapi) +can be found on GitHub. + +The **FastAPI** module provides an easy way to use WebSockets. +Unfortunately, this support is not 'yet' available in the **Odoo** +framework. The challenge is high because the integration of the fastapi +is based on the use of a specific middleware that convert the WSGI +request consumed by odoo to a ASGI request. The question is to know if +it is also possible to develop the same kind of bridge for the +WebSockets and to stream large responses. diff --git a/fastapi/readme/ROADMAP.rst b/fastapi/readme/ROADMAP.rst deleted file mode 100644 index 415a069c..00000000 --- a/fastapi/readme/ROADMAP.rst +++ /dev/null @@ -1,10 +0,0 @@ -The `roadmap `_ -and `known issues `_ can -be found on GitHub. - -The **FastAPI** module provides an easy way to use WebSockets. Unfortunately, this -support is not 'yet' available in the **Odoo** framework. The challenge is high -because the integration of the fastapi is based on the use of a specific middleware -that convert the WSGI request consumed by odoo to a ASGI request. The question -is to know if it is also possible to develop the same kind of bridge for the -WebSockets and to stream large responses. diff --git a/fastapi/readme/USAGE.md b/fastapi/readme/USAGE.md new file mode 100644 index 00000000..0071595d --- /dev/null +++ b/fastapi/readme/USAGE.md @@ -0,0 +1,1394 @@ +## What's building an API with fastapi? + +FastAPI is a modern, fast (high-performance), web framework for building +APIs with Python 3.7+ based on standard Python type hints. This addons +let's you keep advantage of the fastapi framework and use it with Odoo. + +Before you start, we must define some terms: + +- **App**: A FastAPI app is a collection of routes, dependencies, and + other components that can be used to build a web application. +- **Router**: A router is a collection of routes that can be mounted in + an app. +- **Route**: A route is a mapping between an HTTP method and a path, and + defines what should happen when the user requests that path. +- **Dependency**: A dependency is a callable that can be used to get + some information from the user request, or to perform some actions + before the request handler is called. +- **Request**: A request is an object that contains all the information + sent by the user's browser as part of an HTTP request. +- **Response**: A response is an object that contains all the + information that the user's browser needs to build the result page. +- **Handler**: A handler is a function that takes a request and returns + a response. +- **Middleware**: A middleware is a function that takes a request and a + handler, and returns a response. + +The FastAPI framework is based on the following principles: + +- **Fast**: Very high performance, on par with NodeJS and Go (thanks to + Starlette and Pydantic). \[One of the fastest Python frameworks + available\] +- **Fast to code**: Increase the speed to develop features by about 200% + to 300%. +- **Fewer bugs**: Reduce about 40% of human (developer) induced errors. +- **Intuitive**: Great editor support. Completion everywhere. Less time + debugging. +- **Easy**: Designed to be easy to use and learn. Less time reading + docs. +- **Short**: Minimize code duplication. Multiple features from each + parameter declaration. Fewer bugs. +- **Robust**: Get production-ready code. With automatic interactive + documentation. +- **Standards-based**: Based on (and fully compatible with) the open + standards for APIs: OpenAPI (previously known as Swagger) and JSON + Schema. +- **Open Source**: FastAPI is fully open-source, under the MIT license. + +The first step is to install the fastapi addon. You can do it with the +following command: + +> \$ pip install odoo-addon-fastapi + +Once the addon is installed, you can start building your API. The first +thing you need to do is to create a new addon that depends on 'fastapi'. +For example, let's create an addon called *my_demo_api*. + +Then, you need to declare your app by defining a model that inherits +from 'fastapi.endpoint' and add your app name into the app field. For +example: + +``` python +from odoo import fields, models + +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) +``` + +The **'fastapi.endpoint'** model is the base model for all the +endpoints. An endpoint instance is the mount point for a fastapi app +into Odoo. When you create a new endpoint, you can define the app that +you want to mount in the **'app'** field and the path where you want to +mount it in the **'path'** field. + +figure:: static/description/endpoint_create.png + +> FastAPI Endpoint + +Thanks to the **'fastapi.endpoint'** model, you can create as many +endpoints as you want and mount as many apps as you want in each +endpoint. The endpoint is also the place where you can define +configuration parameters for your app. A typical example is the +authentication method that you want to use for your app when accessed at +the endpoint path. + +Now, you can create your first router. For that, you need to define a +global variable into your fastapi_endpoint module called for example +'demo_api_router' + +``` python +from fastapi import APIRouter +from odoo import fields, models + +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + +# create a router +demo_api_router = APIRouter() +``` + +To make your router available to your app, you need to add it to the +list of routers returned by the **\_get_fastapi_routers** method of your +fastapi_endpoint model. + +``` python +from fastapi import APIRouter +from odoo import api, fields, models + +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + + def _get_fastapi_routers(self): + if self.app == "demo": + return [demo_api_router] + return super()._get_fastapi_routers() + +# create a router +demo_api_router = APIRouter() +``` + +Now, you can start adding routes to your router. For example, let's add +a route that returns a list of partners. + +``` python +from typing import Annotated + +from fastapi import APIRouter +from pydantic import BaseModel + +from odoo import api, fields, models +from odoo.api import Environment + +from odoo.addons.fastapi.dependencies import odoo_env + +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + + def _get_fastapi_routers(self): + if self.app == "demo": + return [demo_api_router] + return super()._get_fastapi_routers() + +# create a router +demo_api_router = APIRouter() + +class PartnerInfo(BaseModel): + name: str + email: str + +@demo_api_router.get("/partners", response_model=list[PartnerInfo]) +def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: + return [ + PartnerInfo(name=partner.name, email=partner.email) + for partner in env["res.partner"].search([]) + ] +``` + +Now, you can start your Odoo server, install your addon and create a new +endpoint instance for your app. Once it's done click on the docs url to +access the interactive documentation of your app. + +Before trying to test your app, you need to define on the endpoint +instance the user that will be used to run the app. You can do it by +setting the **'user_id'** field. This information is the most important +one because it's the basis for the security of your app. The user that +you define in the endpoint instance will be used to run the app and to +access the database. This means that the user will be able to access all +the data that he has access to in Odoo. To ensure the security of your +app, you should create a new user that will be used only to run your app +and that will have no access to the database. + +``` xml + + My Demo Endpoint User + my_demo_app_user + + +``` + +At the same time you should create a new group that will be used to +define the access rights of the user that will run your app. This group +should imply the predefined group **'FastAPI Endpoint Runner'**. This +group defines the minimum access rights that the user needs to: + +- access the endpoint instance it belongs to +- access to its own user record +- access to the partner record that is linked to its user record + +``` xml + + My Demo Endpoint Group + + + +``` + +Now, you can test your app. You can do it by clicking on the 'Try it +out' button of the route that you have defined. The result of the +request will be displayed in the 'Response' section and contains the +list of partners. + +Note + +The **'FastAPI Endpoint Runner'** group ensures that the user cannot +access any information others than the 3 ones mentioned above. This +means that for every information that you want to access from your app, +you need to create the proper ACLs and record rules. (see [Managing +security into the route +handlers](#managing-security-into-the-route-handlers)) It's a good +practice to use a dedicated user into a specific group from the +beginning of your project and in your tests. This will force you to +define the proper security rules for your endoints. + +## Dealing with the odoo environment + +The **'odoo.addons.fastapi.dependencies'** module provides a set of +functions that you can use to inject reusable dependencies into your +routes. For example, the **'odoo_env'** function returns the current +odoo environment. You can use it to access the odoo models and the +database from your route handlers. + +``` python +from typing import Annotated + +from odoo.api import Environment +from odoo.addons.fastapi.dependencies import odoo_env + +@demo_api_router.get("/partners", response_model=list[PartnerInfo]) +def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: + return [ + PartnerInfo(name=partner.name, email=partner.email) + for partner in env["res.partner"].search([]) + ] +``` + +As you can see, you can use the **'Depends'** function to inject the +dependency into your route handler. The **'Depends'** function is +provided by the **'fastapi'** framework. You can use it to inject any +dependency into your route handler. As your handler is a python +function, the only way to get access to the odoo environment is to +inject it as a dependency. The fastapi addon provides a set of function +that can be used as dependencies: + +- **'odoo_env'**: Returns the current odoo environment. +- **'fastapi_endpoint'**: Returns the current fastapi endpoint model + instance. +- **'authenticated_partner'**: Returns the authenticated partner. +- **'authenticated_partner_env'**: Returns the current odoo environment + with the authenticated_partner_id into the context. + +By default, the **'odoo_env'** and **'fastapi_endpoint'** dependencies +are available without extra work. + +Note + +Even if 'odoo_env' and 'authenticated_partner_env' returns the current +odoo environment, they are not the same. The 'odoo_env' dependency +returns the environment without any modification while the +'authenticated_partner_env' adds the authenticated partner id into the +context of the environment. As it will be explained in the section +[Managing security into the route +handlers](#managing-security-into-the-route-handlers) dedicated to the +security, the presence of the authenticated partner id into the context +is the key information that will allow you to enforce the security of +your endpoint methods. As consequence, you should always use the +'authenticated_partner_env' dependency instead of the 'odoo_env' +dependency for all the methods that are not public. + +## The dependency injection mechanism + +The **'odoo_env'** dependency relies on a simple implementation that +retrieves the current odoo environment from ContextVar variable +initialized at the start of the request processing by the specific +request dispatcher processing the fastapi requests. + +The **'fastapi_endpoint'** dependency relies on the +'dependency_overrides' mechanism provided by the **'fastapi'** module. +(see the fastapi documentation for more details about the +dependency_overrides mechanism). If you take a look at the current +implementation of the **'fastapi_endpoint'** dependency, you will see +that the method depends of two parameters: **'endpoint_id'** and +**'env'**. Each of these parameters are dependencies themselves. + +``` python +def fastapi_endpoint_id() -> int: + """This method is overriden by default to make the fastapi.endpoint record + available for your endpoint method. To get the fastapi.endpoint record + in your method, you just need to add a dependency on the fastapi_endpoint method + defined below + """ + + +def fastapi_endpoint( + _id: Annotated[int, Depends(fastapi_endpoint_id)], + env: Annotated[Environment, Depends(odoo_env)], +) -> "FastapiEndpoint": + """Return the fastapi.endpoint record""" + return env["fastapi.endpoint"].browse(_id) +``` + +As you can see, one of these dependencies is the +**'fastapi_endpoint_id'** dependency and has no concrete implementation. +This method is used as a contract that must be implemented/provided at +the time the fastapi app is created. Here comes the power of the +dependency_overrides mechanism. + +If you take a look at the **'\_get_app'** method of the +**'FastapiEndpoint'** model, you will see that the +**'fastapi_endpoint_id'** dependency is overriden by registering a +specific method that returns the id of the current fastapi endpoint +model instance for the original method. + +``` python +def _get_app(self) -> FastAPI: + app = FastAPI(**self._prepare_fastapi_endpoint_params()) + for router in self._get_fastapi_routers(): + app.include_router(prefix=self.root_path, router=router) + app.dependency_overrides[dependencies.fastapi_endpoint_id] = partial( + lambda a: a, self.id + ) +``` + +This kind of mechanism is very powerful and allows you to inject any +dependency into your route handlers and moreover, define an abstract +dependency that can be used by any other addon and for which the +implementation could depend on the endpoint configuration. + +## The authentication mechanism + +To make our app not tightly coupled with a specific authentication +mechanism, we will use the **'authenticated_partner'** dependency. As +for the **'fastapi_endpoint'** this dependency depends on an abstract +dependency. + +When you define a route handler, you can inject the +**'authenticated_partner'** dependency as a parameter of your route +handler. + +``` python +from odoo.addons.base.models.res_partner import Partner + + +@demo_api_router.get("/partners", response_model=list[PartnerInfo]) +def get_partners( + env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)] +) -> list[PartnerInfo]: + return [ + PartnerInfo(name=partner.name, email=partner.email) + for partner in env["res.partner"].search([]) + ] +``` + +At this stage, your handler is not tied to a specific authentication +mechanism but only expects to get a partner as a dependency. Depending +on your needs, you can implement different authentication mechanism +available for your app. The fastapi addon provides a default +authentication mechanism using the 'BasicAuth' method. This +authentication mechanism is implemented in the +**'odoo.addons.fastapi.dependencies'** module and relies on +functionalities provided by the **'fastapi.security'** module. + +``` python +def authenticated_partner( + env: Annotated[Environment, Depends(odoo_env)], + security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())], +) -> "res.partner": + """Return the authenticated partner""" + partner = env["res.partner"].search( + [("email", "=", security.username)], limit=1 + ) + if not partner: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + if not partner.check_password(security.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + return partner +``` + +As you can see, the **'authenticated_partner'** dependency relies on the +**'HTTPBasic'** dependency provided by the **'fastapi.security'** +module. In this dummy implementation, we just check that the provided +credentials can be used to authenticate a user in odoo. If the +authentication is successful, we return the partner record linked to the +authenticated user. + +In some cases you could want to implement a more complex authentication +mechanism that could rely on a token or a session. In this case, you can +override the **'authenticated_partner'** dependency by registering a +specific method that returns the authenticated partner. Moreover, you +can make it configurable on the fastapi endpoint model instance. + +To do it, you just need to implement a specific method for each of your +authentication mechanism and allows the user to select one of these +methods when he creates a new fastapi endpoint. Let's say that we want +to allow the authentication by using an api key or via basic auth. Since +basic auth is already implemented, we will only implement the api key +authentication mechanism. + +``` python +from fastapi.security import APIKeyHeader + +def api_key_based_authenticated_partner_impl( + api_key: Annotated[str, Depends( + APIKeyHeader( + name="api-key", + description="In this demo, you can use a user's login as api key.", + ) + )], + env: Annotated[Environment, Depends(odoo_env)], +) -> Partner: + """A dummy implementation that look for a user with the same login + as the provided api key + """ + partner = env["res.users"].search([("login", "=", api_key)], limit=1).partner_id + if not partner: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key" + ) + return partner +``` + +As for the 'BasicAuth' authentication mechanism, we also rely on one of +the native security dependency provided by the **'fastapi.security'** +module. + +Now that we have an implementation for our two authentication +mechanisms, we can allows the user to select one of these authentication +mechanisms by adding a selection field on the fastapi endpoint model. + +``` python +from odoo import fields, models + +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], + string="Authenciation method", + ) +``` + +Note + +A good practice is to prefix specific configuration fields of your app +with the name of your app. This will avoid conflicts with other app when +the 'fastapi.endpoint' model is extended for other 'app'. + +Now that we have a selection field that allows the user to select the +authentication method, we can use the dependency override mechanism to +provide the right implementation of the **'authenticated_partner'** +dependency when the app is instantiated. + +``` python +from odoo.addons.fastapi.dependencies import authenticated_partner +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], + string="Authenciation method", + ) + + def _get_app(self) -> FastAPI: + app = super()._get_app() + if self.app == "demo": + # Here we add the overrides to the authenticated_partner_impl method + # according to the authentication method configured on the demo app + if self.demo_auth_method == "http_basic": + authenticated_partner_impl_override = ( + authenticated_partner_from_basic_auth_user + ) + else: + authenticated_partner_impl_override = ( + api_key_based_authenticated_partner_impl + ) + app.dependency_overrides[ + authenticated_partner_impl + ] = authenticated_partner_impl_override + return app +``` + +To see how the dependency override mechanism works, you can take a look +at the demo app provided by the fastapi addon. If you choose the app +'demo' in the fastapi endpoint form view, you will see that the +authentication method is configurable. You can also see that depending +on the authentication method configured on your fastapi endpoint, the +documentation will change. + +Note + +At time of writing, the dependency override mechanism is not supported +by the fastapi documentation generator. A fix has been proposed and is +waiting to be merged. You can follow the progress of the fix on +[github](https://github.com/tiangolo/fastapi/pull/5452) + +## Managing configuration parameters for your app + +As we have seen in the previous section, you can add configuration +fields on the fastapi endpoint model to allow the user to configure your +app (as for any odoo model you extend). When you need to access these +configuration fields in your route handlers, you can use the +**'odoo.addons.fastapi.dependencies.fastapi_endpoint'** dependency +method to retrieve the 'fastapi.endpoint' record associated to the +current request. + +``` python +from pydantic import BaseModel, Field +from odoo.addons.fastapi.dependencies import fastapi_endpoint + +class EndpointAppInfo(BaseModel): + id: str + name: str + app: str + auth_method: str = Field(alias="demo_auth_method") + root_path: str + model_config = ConfigDict(from_attributes=True) + + + @demo_api_router.get( + "/endpoint_app_info", + response_model=EndpointAppInfo, + dependencies=[Depends(authenticated_partner)], + ) + async def endpoint_app_info( + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + ) -> EndpointAppInfo: + """Returns the current endpoint configuration""" + # This method show you how to get access to current endpoint configuration + # It also show you how you can specify a dependency to force the security + # even if the method doesn't require the authenticated partner as parameter + return EndpointAppInfo.model_validate(endpoint) +``` + +Some of the configuration fields of the fastapi endpoint could impact +the way the app is instantiated. For example, in the previous section, +we have seen that the authentication method configured on the +'fastapi.endpoint' record is used in order to provide the right +implementation of the **'authenticated_partner'** when the app is +instantiated. To ensure that the app is re-instantiated when an element +of the configuration used in the instantiation of the app is modified, +you must override the **'\_fastapi_app_fields'** method to add the name +of the fields that impact the instantiation of the app into the returned +list. + +``` python +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], + string="Authenciation method", + ) + + @api.model + def _fastapi_app_fields(self) -> List[str]: + fields = super()._fastapi_app_fields() + fields.append("demo_auth_method") + return fields +``` + +## Dealing with languages + +The fastapi addon parses the Accept-Language header of the request to +determine the language to use. This parsing is done by respecting the +[RFC 7231 +specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5). +That means that the language is determined by the first language found +in the header that is supported by odoo (with care of the priority +order). If no language is found in the header, the odoo default language +is used. This language is then used to initialize the Odoo's environment +context used by the route handlers. All this makes the management of +languages very easy. You don't have to worry about. This feature is also +documented by default into the generated openapi documentation of your +app to instruct the api consumers how to request a specific language. + +## How to extend an existing app + +When you develop a fastapi app, in a native python app it's not possible +to extend an existing one. This limitation doesn't apply to the fastapi +addon because the fastapi endpoint model is designed to be extended. +However, the way to extend an existing app is not the same as the way to +extend an odoo model. + +First of all, it's important to keep in mind that when you define a +route, you are actually defining a contract between the client and the +server. This contract is defined by the route path, the method (GET, +POST, PUT, DELETE, etc.), the parameters and the response. If you want +to extend an existing app, you must ensure that the contract is not +broken. Any change to the contract will respect the [Liskov substitution +principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle). +This means that the client should not be impacted by the change. + +What does it mean in practice? It means that you can't change the route +path or the method of an existing route. You can't change the name of a +parameter or the type of a response. You can't add a new parameter or a +new response. You can't remove a parameter or a response. If you want to +change the contract, you must create a new route. + +What can you change? + +- You can change the implementation of the route handler. +- You can override the dependencies of the route handler. +- You can add a new route handler. +- You can extend the model used as parameter or as response of the route + handler. + +Let's see how to do that. + +### Changing the implementation of the route handler + +Let's say that you want to change the implementation of the route +handler **'/demo/echo'**. Since a route handler is just a python method, +it could seems a tedious task since we are not into a model method and +therefore we can't take advantage of the Odoo inheritance mechanism. + +However, the fastapi addon provides a way to do that. Thanks to the +**'odoo_env'** dependency method, you can access the current odoo +environment. With this environment, you can access the registry and +therefore the model you want to delegate the implementation to. If you +want to change the implementation of the route handler **'/demo/echo'**, +the only thing you have to do is to inherit from the model where the +implementation is defined and override the method **'echo'**. + +``` python +from pydantic import BaseModel +from fastapi import Depends, APIRouter +from odoo import models +from odoo.addons.fastapi.dependencies import odoo_env + +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self) -> List[APIRouter]: + routers = super()._get_fastapi_routers() + routers.append(demo_api_router) + return routers + +demo_api_router = APIRouter() + +@demo_api_router.get( + "/echo", + response_model=EchoResponse, + dependencies=[Depends(odoo_env)], +) +async def echo( + message: str, + odoo_env: Annotated[Environment, Depends(odoo_env)], +) -> EchoResponse: + """Echo the message""" + return EchoResponse(message=odoo_env["demo.fastapi.endpoint"].echo(message)) + +class EchoResponse(BaseModel): + message: str + +class DemoEndpoint(models.AbstractModel): + + _name = "demo.fastapi.endpoint" + _description = "Demo Endpoint" + + def echo(self, message: str) -> str: + return message + +class DemoEndpointInherit(models.AbstractModel): + + _inherit = "demo.fastapi.endpoint" + + def echo(self, message: str) -> str: + return f"Hello {message}" +``` + +Note + +It's a good programming practice to implement the business logic outside +the route handler. This way, you can easily test your business logic +without having to test the route handler. In the example above, the +business logic is implemented in the method **'echo'** of the model +**'demo.fastapi.endpoint'**. The route handler just delegate the +implementation to this method. + +### Overriding the dependencies of the route handler + +As you've previously seen, the dependency injection mechanism of fastapi +is very powerful. By designing your route handler to rely on +dependencies with a specific functional scope, you can easily change the +implementation of the dependency without having to change the route +handler. With such a design, you can even define abstract dependencies +that must be implemented by the concrete application. This is the case +of the **'authenticated_partner'** dependency in our previous example. +(you can find the implementation of this dependency in the file +**'odoo/addons/fastapi/dependencies.py'** and it's usage in the file +**'odoo/addons/fastapi/models/fastapi_endpoint_demo.py'**) + +### Adding a new route handler + +Let's say that you want to add a new route handler **'/demo/echo2'**. +You could be tempted to add this new route handler in your new addons by +importing the router of the existing app and adding the new route +handler to it. + +``` python +from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router + +@demo_api_router.get( + "/echo2", + response_model=EchoResponse, + dependencies=[Depends(odoo_env)], +) +async def echo2( + message: str, + odoo_env: Annotated[Environment, Depends(odoo_env)], +) -> EchoResponse: + """Echo the message""" + echo = odoo_env["demo.fastapi.endpoint"].echo2(message) + return EchoResponse(message=f"Echo2: {echo}") +``` + +The problem with this approach is that you unconditionally add the new +route handler to the existing app even if the app is called for a +different database where your new addon is not installed. + +The solution is to define a new router and to add it to the list of +routers returned by the method **'\_get_fastapi_routers'** of the model +**'fastapi.endpoint'** you are inheriting from into your new addon. + +``` python +class FastapiEndpoint(models.Model): + + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self) -> List[APIRouter]: + routers = super()._get_fastapi_routers() + if self.app == "demo": + routers.append(additional_demo_api_router) + return routers + +additional_demo_api_router = APIRouter() + +@additional_demo_api_router.get( + "/echo2", + response_model=EchoResponse, + dependencies=[Depends(odoo_env)], +) +async def echo2( + message: str, + odoo_env: Annotated[Environment, Depends(odoo_env)], +) -> EchoResponse: + """Echo the message""" + echo = odoo_env["demo.fastapi.endpoint"].echo2(message) + return EchoResponse(message=f"Echo2: {echo}") +``` + +In this way, the new router is added to the list of routers of your app +only if the app is called for a database where your new addon is +installed. + +### Extending the model used as parameter or as response of the route handler + +The fastapi python library uses the pydantic library to define the +models. By default, once a model is defined, it's not possible to extend +it. However, a companion python library called +[extendable_pydantic](https://pypi.org/project/extendable_pydantic/) +provides a way to use inheritance with pydantic models to extend an +existing model. If used alone, it's your responsibility to instruct this +library the list of extensions to apply to a model and the order to +apply them. This is not very convenient. Fortunately, an dedicated odoo +addon exists to make this process complete transparent. This addon is +called +[odoo-addon-extendable-fastapi](https://pypi.org/project/odoo-addon-extendable-fastapi/). + +When you want to allow other addons to extend a pydantic model, you must +first define the model as an extendable model by using a dedicated +metaclass + +``` python +from pydantic import BaseModel +from extendable_pydantic import ExtendableModelMeta + +class Partner(BaseModel, metaclass=ExtendableModelMeta): + name = 0.1 + model_config = ConfigDict(from_attributes=True) +``` + +As any other pydantic model, you can now use this model as parameter or +as response of a route handler. You can also use all the features of +models defined with pydantic. + +``` python +@demo_api_router.get( + "/partner", + response_model=Location, + dependencies=[Depends(authenticated_partner)], +) +async def partner( + partner: Annotated[ResPartner, Depends(authenticated_partner)], +) -> Partner: + """Return the location""" + return Partner.model_validate(partner) +``` + +If you need to add a new field into the model **'Partner'**, you can +extend it in your new addon by defining a new model that inherits from +the model **'Partner'**. + +``` python +from typing import Optional +from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner + +class PartnerExtended(Partner, extends=Partner): + email: Optional[str] +``` + +If your new addon is installed in a database, a call to the route +handler **'/demo/partner'** will return a response with the new field +**'email'** if a value is provided by the odoo record. + +``` python +{ + "name": "John Doe", + "email": "jhon.doe@acsone.eu" +} +``` + +If your new addon is not installed in a database, a call to the route +handler **'/demo/partner'** will only return the name of the partner. + +``` python +{ + "name": "John Doe" +} +``` + +Note + +The liskov substitution principle has also to be respected. That means +that if you extend a model, you must add new required fields or you must +provide default values for the new optional fields. + +## Managing security into the route handlers + +By default the route handlers are processed using the user configured on +the **'fastapi.endpoint'** model instance. (default is the Public user). +You have seen previously how to define a dependency that will be used to +enforce the authentication of a partner. When a method depends on this +dependency, the 'authenticated_partner_id' key is added to the context +of the partner environment. (If you don't need the partner as dependency +but need to get an environment with the authenticated user, you can use +the dependency 'authenticated_partner_env' instead of +'authenticated_partner'.) + +The fastapi addon extends the 'ir.rule' model to add into the evaluation +context of the security rules the key 'authenticated_partner_id' that +contains the id of the authenticated partner. + +As briefly introduced in a previous section, a good practice when you +develop a fastapi app and you want to protect your data in an efficient +and traceable way is to: + +- create a new user specific to the app but with any access rights. +- create a security group specific to the app and add the user to this + group. (This group must implies the group 'AFastAPI Endpoint Runner' + that give the minimal access rights) +- for each model you want to protect: + - add a 'ir.model.access' record for the model to allow read access to + your model and add the group to the record. + - create a new 'ir.rule' record for the model that restricts the + access to the records of the model to the authenticated partner by + using the key 'authenticated_partner_id' in domain of the rule. (or + to the user defined on the 'fastapi.endpoint' model instance if the + method is public) +- add a dependency on the 'authenticated_partner' to your handlers when + you need to access the authenticated partner or ensure that the + service is called by an authenticated partner. + +``` xml + + My Demo Endpoint User + my_demo_app_user + + + + + My Demo Endpoint Group + + + + + + + My Demo App: access to sale.order + + + + + + + + + + + Sale Order Rule + + [('partner_id', '=', authenticated_partner_id)] + + +``` + +## How to test your fastapi app + +Thanks to the starlette test client, it's possible to test your fastapi +app in a very simple way. With the test client, you can call your route +handlers as if they were real http endpoints. The test client is +available in the **'fastapi.testclient'** module. + +Once again the dependency injection mechanism comes to the rescue by +allowing you to inject into the test client specific implementations of +the dependencies normally provided by the normal processing of the +request by the fastapi app. (for example, you can inject a mock of the +dependency 'authenticated_partner' to test the behavior of your route +handlers when the partner is not authenticated, you can also inject a +mock for the odoo_env etc...) + +The fastapi addon provides a base class for the test cases that you can +use to write your tests. This base class is +**'odoo.fastapi.tests.common.FastAPITransactionCase'**. This class +mainly provides the method **'\_create_test_client'** that you can use +to create a test client for your fastapi app. This method encapsulates +the creation of the test client and the injection of the dependencies. +It also ensures that the odoo environment is make available into the +context of the route handlers. This method is designed to be used when +you need to test your app or when you need to test a specific router +(It's therefore easy to defines tests for routers in an addon that +doesn't provide a fastapi endpoint). + +With this base class, writing a test for a route handler is as simple +as: + +``` python +from odoo.fastapi.tests.common import FastAPITransactionCase + +from odoo.addons.fastapi import dependencies +from odoo.addons.fastapi.routers import demo_router + +class FastAPIDemoCase(FastAPITransactionCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") + cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) + + def test_hello_world(self) -> None: + with self._create_test_client(router=demo_router) as test_client: + response: Response = test_client.get("/demo/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(response.json(), {"Hello": "World"}) +``` + +In the previous example, we created a test client for the demo_router. +We could have created a test client for the whole app by not specifying +the router but the app instead. + +``` python +from odoo.fastapi.tests.common import FastAPITransactionCase + +from odoo.addons.fastapi import dependencies +from odoo.addons.fastapi.routers import demo_router + +class FastAPIDemoCase(FastAPITransactionCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") + cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) + + def test_hello_world(self) -> None: + demo_endpoint = self.env.ref("fastapi.fastapi_endpoint_demo") + with self._create_test_client(app=demo_endpoint._get_app()) as test_client: + response: Response = test_client.get(f"{demo_endpoint.root_path}/demo/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(response.json(), {"Hello": "World"}) +``` + +## Overall considerations when you develop an fastapi app + +Developing a fastapi app requires to follow some good practices to +ensure that the app is robust and easy to maintain. Here are some of +them: + +- A route handler must be as simple as possible. It must not contain any + business logic. The business logic must be implemented into the + service layer. The route handler must only call the service layer and + return the result of the service layer. To ease extension on your + business logic, your service layer can be implemented as an odoo + abstract model that can be inherited by other addons. +- A route handler should not expose the internal data structure and api + of Odoo. It should provide the api that is needed by the client. More + widely, an app provides a set of services that address a set of use + cases specific to a well defined functional domain. You must always + keep in mind that your api will remain the same for a long time even + if you upgrade your odoo version of modify your business logic. +- A route handler is a transactional unit of work. When you design your + api you must ensure that the completeness of a use case is guaranteed + by a single transaction. If you need to perform several transactions + to complete a use case, you introduce a risk of inconsistency in your + data or extra complexity in your client code. +- Properly handle the errors. The route handler must return a proper + error response when an error occurs. The error response must be + consistent with the rest of the api. The error response must be + documented in the api documentation. By default, the + **'odoo-addon-fastapi'** module handles the common exception types + defined in the **'odoo.exceptions'** module and returns a proper error + response with the corresponding http status code. An error in the + route handler must always return an error response with a http status + code different from 200. The error response must contain a human + readable message that can be displayed to the user. The error response + can also contain a machine readable code that can be used by the + client to handle the error in a specific way. +- When you design your json document through the pydantic models, you + must use the appropriate data types. For example, you must use the + data type **'datetime.date'** to represent a date and not a string. + You must also properly define the constraints on the fields. For + example, if a field is optional, you must use the data type + **'typing.Optional'**. [pydantic](https://docs.pydantic.dev/) provides + everything you need to properly define your json document. +- Always use an appropriate pydantic model as request and/or response + for your route handler. Constraints on the fields of the pydantic + model must apply to the specific use case. For example, if your route + handler is used to create a sale order, the pydantic model must not + contain the field 'id' because the id of the sale order will be + generated by the route handler. But if the id is required afterwords, + the pydantic model for the response must contain the field 'id' as + required. +- Uses descriptive property names in your json documents. For example, + avoid the use of documents providing a flat list of key value pairs. +- Be consistent in the naming of your fields into your json documents. + For example, if you use 'id' to represent the id of a sale order, you + must use 'id' to represent the id of all the other objects. +- Be consistent in the naming style of your fields. Always prefer + underscore to camel case. +- Always use plural for the name of the fields that contain a list of + items. For example, if you have a field 'lines' that contains a list + of sale order lines, you must use 'lines' and not 'line'. +- You can't expect that a client will provide you the identifier of a + specific record in odoo (for example the id of a carrier) if you don't + provide a specific route handler to retrieve the list of available + records. Sometimes, the client must share with odoo the identity of a + specific record to be able to perform an appropriate action specific + to this record (for example, the processing of a payment is different + for each payment acquirer). In this case, you must provide a specific + attribute that allows both the client and odoo to identify the record. + The field 'provider' on a payment acquirer allows you to identify a + specific record in odoo. This kind of approach allows both the client + and odoo to identify the record without having to rely on the id of + the record. (This will ensure that the client will not break if the id + of the record is changed in odoo for example when tests are run on an + other database). +- Always use the same name for the same kind of object. For example, if + you have a field 'lines' that contains a list of sale order lines, you + must use the same name for the same kind of object in all the other + json documents. +- Manage relations between objects in your json documents the same way. + By default, you should return the id of the related object in the json + document. But this is not always possible or convenient, so you can + also return the related object in the json document. The main + advantage of returning the id of the related object is that it allows + you to avoid the [n+1 + problem](https://restfulapi.net/rest-api-n-1-problem/) . The main + advantage of returning the related object in the json document is that + it allows you to avoid an extra call to retrieve the related object. + By keeping in mind the pros and cons of each approach, you can choose + the best one for your use case. Once it's done, you must be consistent + in the way you manage the relations of the same object. +- It's not always a good idea to name your fields into your json + documents with the same name as the fields of the corresponding odoo + model. For example, in your document representing a sale order, you + must not use the name 'order_line' for the field that contains the + list of sale order lines. The name 'order_line' in addition to being + confusing and not consistent with the best practices, is not + auto-descriptive. The name 'lines' is much better. +- Keep a defensive programming approach. If you provide a route handler + that returns a list of records, you must ensure that the computation + of the list is not too long or will not drain your server resources. + For example, for search route handlers, you must ensure that the + search is limited to a reasonable number of records by default. +- As a corollary of the previous point, a search handler must always use + the pagination mechanism with a reasonable default page size. The + result list must be enclosed in a json document that contains the + count of records into the system matching your search criteria and the + list of records for the given page and size. +- Use plural for the name of a service. For example, if you provide a + service that allows you to manage the sale orders, you must use the + name 'sale_orders' and not 'sale_order'. +- ... and many more. + +We could write a book about the best practices to follow when you design +your api but we will stop here. This list is the result of our +experience at [ACSONE SA/NV](https://acsone.eu) and it evolves over +time. It's a kind of rescue kit that we would provide to a new developer +that starts to design an api. This kit must be accompanied with the +reading of some useful resources link like the [REST +Guidelines](https://www.belgif.be/specification/rest/api-guide/). On a +technical level, the [fastapi +documentation](https://fastapi.tiangolo.com/) provides a lot of useful +information as well, with a lot of examples. Last but not least, the +[pydantic](https://docs.pydantic.dev/) documentation is also very +useful. + +## Miscellaneous + +### Development of a search route handler + +The **'odoo-addon-fastapi'** module provides 2 useful piece of code to +help you be consistent when writing a route handler for a search route. + +1. A dependency method to use to specify the pagination parameters in + the same way for all the search route handlers: + **'odoo.addons.fastapi.paging'**. +2. A PagedCollection pydantic model to use to return the result of a + search route handler enclosed in a json document that contains the + count of records. + +``` python +from typing import Annotated +from pydantic import BaseModel + +from odoo.api import Environment +from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env +from odoo.addons.fastapi.schemas import PagedCollection, Paging + +class SaleOrder(BaseModel): + id: int + name: str + model_config = ConfigDict(from_attributes=True) + + +@router.get( + "/sale_orders", + response_model=PagedCollection[SaleOrder], + response_model_exclude_unset=True, +) +def get_sale_orders( + paging: Annotated[Paging, Depends(paging)], + env: Annotated[Environment, Depends(authenticated_partner_env)], +) -> PagedCollection[SaleOrder]: + """Get the list of sale orders.""" + count = env["sale.order"].search_count([]) + orders = env["sale.order"].search([], limit=paging.limit, offset=paging.offset) + return PagedCollection[SaleOrder]( + count=count, + items=[SaleOrder.model_validate(order) for order in orders], + ) +``` + +Note + +The **'odoo.addons.fastapi.schemas.Paging'** and +**'odoo.addons.fastapi.schemas.PagedCollection'** pydantic models are +not designed to be extended to not introduce a dependency between the +**'odoo-addon-fastapi'** module and the **'odoo-addon-extendable'** + +### Customization of the error handling + +The error handling a very important topic in the design of the fastapi +integration with odoo. It must ensure that the error messages are +properly return to the client and that the transaction is properly roll +backed. The **'fastapi'** module provides a way to register custom error +handlers. The **'odoo.addons.fastapi.error_handlers'** module provides +the default error handlers that are registered by default when a new +instance of the **'FastAPI'** class is created. When an app is +initialized in 'fastapi.endpoint' model, the method +\_get_app_exception_handlers is called to get a dictionary of error +handlers. This method is designed to be overridden in a custom module to +provide custom error handlers. You can override the handler for a +specific exception class or you can add a new handler for a new +exception or even replace all the handlers by your own handlers. +Whatever you do, you must ensure that the transaction is properly roll +backed. + +Some could argue that the error handling can't be extended since the +error handlers are global method not defined in an odoo model. Since the +method providing the the error handlers definitions is defined on the +'fastapi.endpoint' model, it's not a problem at all, you just need to +think another way to do it that by inheritance. + +A solution could be to develop you own error handler to be able to +process the error and chain the call to the default error handler. + +``` python +class MyCustomErrorHandler(): + def __init__(self, next_handler): + self.next_handler = next_handler + + def __call__(self, request: Request, exc: Exception) -> JSONResponse: + # do something with the error + response = self.next_handler(request, exc) + # do something with the response + return response +``` + +With this solution, you can now register your custom error handler by +overriding the method \_get_app_exception_handlers in your custom +module. + +``` python +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + def _get_app_exception_handlers( + self, + ) -> Dict[ + Union[int, Type[Exception]], + Callable[[Request, Exception], Union[Response, Awaitable[Response]]], + ]: + handlers = super()._get_app_exception_handlers() + access_error_handler = handlers.get(odoo.exceptions.AccessError) + handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler) + return handlers +``` + +In the previous example, we extend the error handler for the +'AccessError' exception for all the endpoints. You can do the same for a +specific app by checking the 'app' field of the 'fastapi.endpoint' +record before registering your custom error handler. + +### FastAPI addons directory structure + +When you develop a new addon to expose an api with fastapi, it's a good +practice to follow the same directory structure and naming convention +for the files related to the api. It will help you to easily find the +files related to the api and it will help the other developers to +understand your code. + +Here is the directory structure that we recommend. It's based on +practices that are used in the python community when developing a +fastapi app. + +``` +. +├── x_api +│   ├── data +│   │ ├── ... .xml +│   ├── demo +│   │ ├── ... .xml +│   ├── i18n +│   │ ├── ... .po +│   ├── models +│   │ ├── __init__.py +│   │ ├── fastapi_endpoint.py # your app +│   │ └── ... .py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── ... .py +│   ├── schemas | schemas.py +│   │ ├── __init__.py +│   │ ├── my_model.py # pydantic model +│   │ └── ... .py +│   ├── security +│   │ ├── ... .xml +│   ├── views +│   │ ├── ... .xml +│   ├── __init__.py +│   ├── __manifest__.py +│   ├── dependencies.py # custom dependencies +│   ├── error_handlers.py # custom error handlers +``` + +- The **'models'** directory contains the odoo models. When you define a + new app, as for the others addons, you will add your new model + inheriting from the **'fastapi.endpoint'** model in this directory. + +- The **'routers'** directory contains the fastapi routers. You will add + your new routers in this directory. Each route starting with the same + prefix should be grouped in the same file. For example, all the routes + starting with '/items' should be defined in the **'items.py'** file. + The **'\_\_init\_\_.py'** file in this directory is used to import all + the routers defined in the directory and create a global router that + can be used in an app. For example, in your **'items.py'** file, you + will define a router like this: + + ``` python + router = APIRouter(tags=["items"]) + + router.get("/items", response_model=List[Item]) + def list_items(): + pass + ``` + + In the **'\_\_init\_\_.py'** file, you will import the router and add + it to the global router or your addon. + + ``` python + from fastapi import APIRouter + + from .items import router as items_router + + router = APIRouter() + router.include_router(items_router) + ``` + +- The **'schemas.py'** will be used to define the pydantic models. For + complex APIs with a lot of models, it will be better to create a + **'schemas'** directory and split the models in different files. The + **'\_\_init\_\_.py'** file in this directory will be used to import + all the models defined in the directory. For example, in your + **'my_model.py'** file, you will define a model like this: + + ``` python + from pydantic import BaseModel + + class MyModel(BaseModel): + name: str + description: str = None + ``` + + In the **'\_\_init\_\_.py'** file, you will import the model's classes + from the files in the directory. + + ``` python + from .my_model import MyModel + ``` + + This will allow to always import the models from the schemas module + whatever the models are spread across different files or defined in + the **'schemas.py'** file. + + ``` python + from x_api_addon.schemas import MyModel + ``` + +- The **'dependencies.py'** file contains the custom dependencies that + you will use in your routers. For example, you can define a dependency + to check the access rights of the user. + +- The **'error_handlers.py'** file contains the custom error handlers + that you will use in your routers. The **'odoo-addon-fastapi'** module + provides the default error handlers for the common odoo exceptions. + Chance are that you will not need to define your own error handlers. + But if you need to do it, you can define them in this file. + +## What's next? + +The **'odoo-addon-fastapi'** module is still in its early stage of +development. It will evolve over time to integrate your feedback and to +provide the missing features. It's now up to you to try it and to +provide your feedback. diff --git a/fastapi/readme/USAGE.rst b/fastapi/readme/USAGE.rst deleted file mode 100644 index ef6f8240..00000000 --- a/fastapi/readme/USAGE.rst +++ /dev/null @@ -1,1375 +0,0 @@ -What's building an API with fastapi? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -FastAPI is a modern, fast (high-performance), web framework for building APIs -with Python 3.7+ based on standard Python type hints. This addons let's you -keep advantage of the fastapi framework and use it with Odoo. - -Before you start, we must define some terms: - -* **App**: A FastAPI app is a collection of routes, dependencies, and other - components that can be used to build a web application. -* **Router**: A router is a collection of routes that can be mounted in an - app. -* **Route**: A route is a mapping between an HTTP method and a path, and - defines what should happen when the user requests that path. -* **Dependency**: A dependency is a callable that can be used to get some - information from the user request, or to perform some actions before the - request handler is called. -* **Request**: A request is an object that contains all the information - sent by the user's browser as part of an HTTP request. -* **Response**: A response is an object that contains all the information - that the user's browser needs to build the result page. -* **Handler**: A handler is a function that takes a request and returns a - response. -* **Middleware**: A middleware is a function that takes a request and a - handler, and returns a response. - -The FastAPI framework is based on the following principles: - -* **Fast**: Very high performance, on par with NodeJS and Go (thanks to Starlette - and Pydantic). [One of the fastest Python frameworks available] -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. -* **Intuitive**: Great editor support. Completion everywhere. Less time - debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter - declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards - for APIs: OpenAPI (previously known as Swagger) and JSON Schema. -* **Open Source**: FastAPI is fully open-source, under the MIT license. - -The first step is to install the fastapi addon. You can do it with the -following command: - - $ pip install odoo-addon-fastapi - -Once the addon is installed, you can start building your API. The first thing -you need to do is to create a new addon that depends on 'fastapi'. For example, -let's create an addon called *my_demo_api*. - -Then, you need to declare your app by defining a model that inherits from -'fastapi.endpoint' and add your app name into the app field. For example: - -.. code-block:: python - - from odoo import fields, models - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - -The **'fastapi.endpoint'** model is the base model for all the endpoints. An endpoint -instance is the mount point for a fastapi app into Odoo. When you create a new -endpoint, you can define the app that you want to mount in the **'app'** field -and the path where you want to mount it in the **'path'** field. - -figure:: static/description/endpoint_create.png - - FastAPI Endpoint - -Thanks to the **'fastapi.endpoint'** model, you can create as many endpoints as -you want and mount as many apps as you want in each endpoint. The endpoint is -also the place where you can define configuration parameters for your app. A -typical example is the authentication method that you want to use for your app -when accessed at the endpoint path. - -Now, you can create your first router. For that, you need to define a global -variable into your fastapi_endpoint module called for example 'demo_api_router' - -.. code-block:: python - - from fastapi import APIRouter - from odoo import fields, models - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - - # create a router - demo_api_router = APIRouter() - - -To make your router available to your app, you need to add it to the list of routers -returned by the **_get_fastapi_routers** method of your fastapi_endpoint model. - -.. code-block:: python - - from fastapi import APIRouter - from odoo import api, fields, models - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - - def _get_fastapi_routers(self): - if self.app == "demo": - return [demo_api_router] - return super()._get_fastapi_routers() - - # create a router - demo_api_router = APIRouter() - -Now, you can start adding routes to your router. For example, let's add a route -that returns a list of partners. - -.. code-block:: python - - from typing import Annotated - - from fastapi import APIRouter - from pydantic import BaseModel - - from odoo import api, fields, models - from odoo.api import Environment - - from odoo.addons.fastapi.dependencies import odoo_env - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - - def _get_fastapi_routers(self): - if self.app == "demo": - return [demo_api_router] - return super()._get_fastapi_routers() - - # create a router - demo_api_router = APIRouter() - - class PartnerInfo(BaseModel): - name: str - email: str - - @demo_api_router.get("/partners", response_model=list[PartnerInfo]) - def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: - return [ - PartnerInfo(name=partner.name, email=partner.email) - for partner in env["res.partner"].search([]) - ] - -Now, you can start your Odoo server, install your addon and create a new endpoint -instance for your app. Once it's done click on the docs url to access the -interactive documentation of your app. - -Before trying to test your app, you need to define on the endpoint instance the -user that will be used to run the app. You can do it by setting the **'user_id'** -field. This information is the most important one because it's the basis for -the security of your app. The user that you define in the endpoint instance -will be used to run the app and to access the database. This means that the -user will be able to access all the data that he has access to in Odoo. To ensure -the security of your app, you should create a new user that will be used only -to run your app and that will have no access to the database. - -.. code-block:: xml - - - My Demo Endpoint User - my_demo_app_user - - - -At the same time you should create a new group that will be used to define the -access rights of the user that will run your app. This group should imply -the predefined group **'FastAPI Endpoint Runner'**. This group defines the -minimum access rights that the user needs to: - -* access the endpoint instance it belongs to -* access to its own user record -* access to the partner record that is linked to its user record - -.. code-block:: xml - - - My Demo Endpoint Group - - - - - -Now, you can test your app. You can do it by clicking on the 'Try it out' button -of the route that you have defined. The result of the request will be displayed -in the 'Response' section and contains the list of partners. - -.. note:: - The **'FastAPI Endpoint Runner'** group ensures that the user cannot access any - information others than the 3 ones mentioned above. This means that for every - information that you want to access from your app, you need to create the - proper ACLs and record rules. (see `Managing security into the route handlers`_) - It's a good practice to use a dedicated user into a specific group from the - beginning of your project and in your tests. This will force you to define - the proper security rules for your endoints. - -Dealing with the odoo environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The **'odoo.addons.fastapi.dependencies'** module provides a set of functions that you can use -to inject reusable dependencies into your routes. For example, the **'odoo_env'** -function returns the current odoo environment. You can use it to access the -odoo models and the database from your route handlers. - -.. code-block:: python - - from typing import Annotated - - from odoo.api import Environment - from odoo.addons.fastapi.dependencies import odoo_env - - @demo_api_router.get("/partners", response_model=list[PartnerInfo]) - def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: - return [ - PartnerInfo(name=partner.name, email=partner.email) - for partner in env["res.partner"].search([]) - ] - -As you can see, you can use the **'Depends'** function to inject the dependency -into your route handler. The **'Depends'** function is provided by the -**'fastapi'** framework. You can use it to inject any dependency into your route -handler. As your handler is a python function, the only way to get access to -the odoo environment is to inject it as a dependency. The fastapi addon provides -a set of function that can be used as dependencies: - -* **'odoo_env'**: Returns the current odoo environment. -* **'fastapi_endpoint'**: Returns the current fastapi endpoint model instance. -* **'authenticated_partner'**: Returns the authenticated partner. -* **'authenticated_partner_env'**: Returns the current odoo environment with the - authenticated_partner_id into the context. - -By default, the **'odoo_env'** and **'fastapi_endpoint'** dependencies are -available without extra work. - -.. note:: - Even if 'odoo_env' and 'authenticated_partner_env' returns the current odoo - environment, they are not the same. The 'odoo_env' dependency returns the - environment without any modification while the 'authenticated_partner_env' - adds the authenticated partner id into the context of the environment. As it will - be explained in the section `Managing security into the route handlers`_ dedicated - to the security, the presence of the authenticated partner id into the context - is the key information that will allow you to enforce the security of your endpoint - methods. As consequence, you should always use the 'authenticated_partner_env' - dependency instead of the 'odoo_env' dependency for all the methods that are - not public. - -The dependency injection mechanism -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The **'odoo_env'** dependency relies on a simple implementation that retrieves -the current odoo environment from ContextVar variable initialized at the start -of the request processing by the specific request dispatcher processing the -fastapi requests. - -The **'fastapi_endpoint'** dependency relies on the 'dependency_overrides' mechanism -provided by the **'fastapi'** module. (see the fastapi documentation for more -details about the dependency_overrides mechanism). If you take a look at the -current implementation of the **'fastapi_endpoint'** dependency, you will see -that the method depends of two parameters: **'endpoint_id'** and **'env'**. Each -of these parameters are dependencies themselves. - -.. code-block:: python - - def fastapi_endpoint_id() -> int: - """This method is overriden by default to make the fastapi.endpoint record - available for your endpoint method. To get the fastapi.endpoint record - in your method, you just need to add a dependency on the fastapi_endpoint method - defined below - """ - - - def fastapi_endpoint( - _id: Annotated[int, Depends(fastapi_endpoint_id)], - env: Annotated[Environment, Depends(odoo_env)], - ) -> "FastapiEndpoint": - """Return the fastapi.endpoint record""" - return env["fastapi.endpoint"].browse(_id) - - -As you can see, one of these dependencies is the **'fastapi_endpoint_id'** -dependency and has no concrete implementation. This method is used as a contract -that must be implemented/provided at the time the fastapi app is created. -Here comes the power of the dependency_overrides mechanism. - -If you take a look at the **'_get_app'** method of the **'FastapiEndpoint'** model, -you will see that the **'fastapi_endpoint_id'** dependency is overriden by -registering a specific method that returns the id of the current fastapi endpoint -model instance for the original method. - -.. code-block:: python - - def _get_app(self) -> FastAPI: - app = FastAPI(**self._prepare_fastapi_endpoint_params()) - for router in self._get_fastapi_routers(): - app.include_router(prefix=self.root_path, router=router) - app.dependency_overrides[dependencies.fastapi_endpoint_id] = partial( - lambda a: a, self.id - ) - -This kind of mechanism is very powerful and allows you to inject any dependency -into your route handlers and moreover, define an abstract dependency that can be -used by any other addon and for which the implementation could depend on the -endpoint configuration. - -The authentication mechanism -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To make our app not tightly coupled with a specific authentication mechanism, -we will use the **'authenticated_partner'** dependency. As for the -**'fastapi_endpoint'** this dependency depends on an abstract dependency. - -When you define a route handler, you can inject the **'authenticated_partner'** -dependency as a parameter of your route handler. - -.. code-block:: python - - from odoo.addons.base.models.res_partner import Partner - - - @demo_api_router.get("/partners", response_model=list[PartnerInfo]) - def get_partners( - env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)] - ) -> list[PartnerInfo]: - return [ - PartnerInfo(name=partner.name, email=partner.email) - for partner in env["res.partner"].search([]) - ] - - -At this stage, your handler is not tied to a specific authentication mechanism -but only expects to get a partner as a dependency. Depending on your needs, you -can implement different authentication mechanism available for your app. -The fastapi addon provides a default authentication mechanism using the -'BasicAuth' method. This authentication mechanism is implemented in the -**'odoo.addons.fastapi.dependencies'** module and relies on functionalities provided -by the **'fastapi.security'** module. - -.. code-block:: python - - def authenticated_partner( - env: Annotated[Environment, Depends(odoo_env)], - security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())], - ) -> "res.partner": - """Return the authenticated partner""" - partner = env["res.partner"].search( - [("email", "=", security.username)], limit=1 - ) - if not partner: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Basic"}, - ) - if not partner.check_password(security.password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Basic"}, - ) - return partner - -As you can see, the **'authenticated_partner'** dependency relies on the -**'HTTPBasic'** dependency provided by the **'fastapi.security'** module. -In this dummy implementation, we just check that the provided credentials -can be used to authenticate a user in odoo. If the authentication is successful, -we return the partner record linked to the authenticated user. - -In some cases you could want to implement a more complex authentication mechanism -that could rely on a token or a session. In this case, you can override the -**'authenticated_partner'** dependency by registering a specific method that -returns the authenticated partner. Moreover, you can make it configurable on -the fastapi endpoint model instance. - -To do it, you just need to implement a specific method for each of your -authentication mechanism and allows the user to select one of these methods -when he creates a new fastapi endpoint. Let's say that we want to allow the -authentication by using an api key or via basic auth. Since basic auth is already -implemented, we will only implement the api key authentication mechanism. - -.. code-block:: python - - from fastapi.security import APIKeyHeader - - def api_key_based_authenticated_partner_impl( - api_key: Annotated[str, Depends( - APIKeyHeader( - name="api-key", - description="In this demo, you can use a user's login as api key.", - ) - )], - env: Annotated[Environment, Depends(odoo_env)], - ) -> Partner: - """A dummy implementation that look for a user with the same login - as the provided api key - """ - partner = env["res.users"].search([("login", "=", api_key)], limit=1).partner_id - if not partner: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key" - ) - return partner - -As for the 'BasicAuth' authentication mechanism, we also rely on one of the native -security dependency provided by the **'fastapi.security'** module. - -Now that we have an implementation for our two authentication mechanisms, we -can allows the user to select one of these authentication mechanisms by adding -a selection field on the fastapi endpoint model. - -.. code-block:: python - - from odoo import fields, models - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - demo_auth_method = fields.Selection( - selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], - string="Authenciation method", - ) - -.. note:: - A good practice is to prefix specific configuration fields of your app with - the name of your app. This will avoid conflicts with other app when the - 'fastapi.endpoint' model is extended for other 'app'. - -Now that we have a selection field that allows the user to select the -authentication method, we can use the dependency override mechanism to -provide the right implementation of the **'authenticated_partner'** dependency -when the app is instantiated. - -.. code-block:: python - - from odoo.addons.fastapi.dependencies import authenticated_partner - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - demo_auth_method = fields.Selection( - selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], - string="Authenciation method", - ) - - def _get_app(self) -> FastAPI: - app = super()._get_app() - if self.app == "demo": - # Here we add the overrides to the authenticated_partner_impl method - # according to the authentication method configured on the demo app - if self.demo_auth_method == "http_basic": - authenticated_partner_impl_override = ( - authenticated_partner_from_basic_auth_user - ) - else: - authenticated_partner_impl_override = ( - api_key_based_authenticated_partner_impl - ) - app.dependency_overrides[ - authenticated_partner_impl - ] = authenticated_partner_impl_override - return app - - -To see how the dependency override mechanism works, you can take a look at the -demo app provided by the fastapi addon. If you choose the app 'demo' in the -fastapi endpoint form view, you will see that the authentication method -is configurable. You can also see that depending on the authentication method -configured on your fastapi endpoint, the documentation will change. - -.. note:: - At time of writing, the dependency override mechanism is not supported by - the fastapi documentation generator. A fix has been proposed and is waiting - to be merged. You can follow the progress of the fix on `github - `_ - -Managing configuration parameters for your app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -As we have seen in the previous section, you can add configuration fields -on the fastapi endpoint model to allow the user to configure your app (as for -any odoo model you extend). When you need to access these configuration fields -in your route handlers, you can use the **'odoo.addons.fastapi.dependencies.fastapi_endpoint'** -dependency method to retrieve the 'fastapi.endpoint' record associated to the -current request. - -.. code-block:: python - - from pydantic import BaseModel, Field - from odoo.addons.fastapi.dependencies import fastapi_endpoint - - class EndpointAppInfo(BaseModel): - id: str - name: str - app: str - auth_method: str = Field(alias="demo_auth_method") - root_path: str - model_config = ConfigDict(from_attributes=True) - - - @demo_api_router.get( - "/endpoint_app_info", - response_model=EndpointAppInfo, - dependencies=[Depends(authenticated_partner)], - ) - async def endpoint_app_info( - endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], - ) -> EndpointAppInfo: - """Returns the current endpoint configuration""" - # This method show you how to get access to current endpoint configuration - # It also show you how you can specify a dependency to force the security - # even if the method doesn't require the authenticated partner as parameter - return EndpointAppInfo.model_validate(endpoint) - -Some of the configuration fields of the fastapi endpoint could impact the way -the app is instantiated. For example, in the previous section, we have seen -that the authentication method configured on the 'fastapi.endpoint' record is -used in order to provide the right implementation of the **'authenticated_partner'** -when the app is instantiated. To ensure that the app is re-instantiated when -an element of the configuration used in the instantiation of the app is -modified, you must override the **'_fastapi_app_fields'** method to add the -name of the fields that impact the instantiation of the app into the returned -list. - -.. code-block:: python - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - app: str = fields.Selection( - selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} - ) - demo_auth_method = fields.Selection( - selection=[("api_key", "Api Key"), ("http_basic", "HTTP Bacic")], - string="Authenciation method", - ) - - @api.model - def _fastapi_app_fields(self) -> List[str]: - fields = super()._fastapi_app_fields() - fields.append("demo_auth_method") - return fields - -Dealing with languages -~~~~~~~~~~~~~~~~~~~~~~ - -The fastapi addon parses the Accept-Language header of the request to determine -the language to use. This parsing is done by respecting the `RFC 7231 specification -`_. That means that -the language is determined by the first language found in the header that is -supported by odoo (with care of the priority order). If no language is found in -the header, the odoo default language is used. This language is then used to -initialize the Odoo's environment context used by the route handlers. All this -makes the management of languages very easy. You don't have to worry about. This -feature is also documented by default into the generated openapi documentation -of your app to instruct the api consumers how to request a specific language. - - -How to extend an existing app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you develop a fastapi app, in a native python app it's not possible -to extend an existing one. This limitation doesn't apply to the fastapi addon -because the fastapi endpoint model is designed to be extended. However, the -way to extend an existing app is not the same as the way to extend an odoo model. - -First of all, it's important to keep in mind that when you define a route, you -are actually defining a contract between the client and the server. This -contract is defined by the route path, the method (GET, POST, PUT, DELETE, -etc.), the parameters and the response. If you want to extend an existing app, -you must ensure that the contract is not broken. Any change to the contract -will respect the `Liskov substitution principle -`_. This means -that the client should not be impacted by the change. - -What does it mean in practice? It means that you can't change the route path -or the method of an existing route. You can't change the name of a parameter -or the type of a response. You can't add a new parameter or a new response. -You can't remove a parameter or a response. If you want to change the contract, -you must create a new route. - -What can you change? - -* You can change the implementation of the route handler. -* You can override the dependencies of the route handler. -* You can add a new route handler. -* You can extend the model used as parameter or as response of the route handler. - -Let's see how to do that. - -Changing the implementation of the route handler -================================================ - -Let's say that you want to change the implementation of the route handler -**'/demo/echo'**. Since a route handler is just a python method, it could seems -a tedious task since we are not into a model method and therefore we can't -take advantage of the Odoo inheritance mechanism. - -However, the fastapi addon provides a way to do that. Thanks to the **'odoo_env'** -dependency method, you can access the current odoo environment. With this -environment, you can access the registry and therefore the model you want to -delegate the implementation to. If you want to change the implementation of -the route handler **'/demo/echo'**, the only thing you have to do is to -inherit from the model where the implementation is defined and override the -method **'echo'**. - -.. code-block:: python - - from pydantic import BaseModel - from fastapi import Depends, APIRouter - from odoo import models - from odoo.addons.fastapi.dependencies import odoo_env - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - def _get_fastapi_routers(self) -> List[APIRouter]: - routers = super()._get_fastapi_routers() - routers.append(demo_api_router) - return routers - - demo_api_router = APIRouter() - - @demo_api_router.get( - "/echo", - response_model=EchoResponse, - dependencies=[Depends(odoo_env)], - ) - async def echo( - message: str, - odoo_env: Annotated[Environment, Depends(odoo_env)], - ) -> EchoResponse: - """Echo the message""" - return EchoResponse(message=odoo_env["demo.fastapi.endpoint"].echo(message)) - - class EchoResponse(BaseModel): - message: str - - class DemoEndpoint(models.AbstractModel): - - _name = "demo.fastapi.endpoint" - _description = "Demo Endpoint" - - def echo(self, message: str) -> str: - return message - - class DemoEndpointInherit(models.AbstractModel): - - _inherit = "demo.fastapi.endpoint" - - def echo(self, message: str) -> str: - return f"Hello {message}" - - -.. note:: - - It's a good programming practice to implement the business logic outside - the route handler. This way, you can easily test your business logic without - having to test the route handler. In the example above, the business logic - is implemented in the method **'echo'** of the model **'demo.fastapi.endpoint'**. - The route handler just delegate the implementation to this method. - - -Overriding the dependencies of the route handler -================================================ - -As you've previously seen, the dependency injection mechanism of fastapi is -very powerful. By designing your route handler to rely on dependencies with -a specific functional scope, you can easily change the implementation of the -dependency without having to change the route handler. With such a design, you -can even define abstract dependencies that must be implemented by the concrete -application. This is the case of the **'authenticated_partner'** dependency in our -previous example. (you can find the implementation of this dependency in the -file **'odoo/addons/fastapi/dependencies.py'** and it's usage in the file -**'odoo/addons/fastapi/models/fastapi_endpoint_demo.py'**) - -Adding a new route handler -========================== - -Let's say that you want to add a new route handler **'/demo/echo2'**. -You could be tempted to add this new route handler in your new addons by -importing the router of the existing app and adding the new route handler to -it. - -.. code-block:: python - - from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router - - @demo_api_router.get( - "/echo2", - response_model=EchoResponse, - dependencies=[Depends(odoo_env)], - ) - async def echo2( - message: str, - odoo_env: Annotated[Environment, Depends(odoo_env)], - ) -> EchoResponse: - """Echo the message""" - echo = odoo_env["demo.fastapi.endpoint"].echo2(message) - return EchoResponse(message=f"Echo2: {echo}") - -The problem with this approach is that you unconditionally add the new route -handler to the existing app even if the app is called for a different database -where your new addon is not installed. - -The solution is to define a new router and to add it to the list of routers -returned by the method **'_get_fastapi_routers'** of the model -**'fastapi.endpoint'** you are inheriting from into your new addon. - -.. code-block:: python - - class FastapiEndpoint(models.Model): - - _inherit = "fastapi.endpoint" - - def _get_fastapi_routers(self) -> List[APIRouter]: - routers = super()._get_fastapi_routers() - if self.app == "demo": - routers.append(additional_demo_api_router) - return routers - - additional_demo_api_router = APIRouter() - - @additional_demo_api_router.get( - "/echo2", - response_model=EchoResponse, - dependencies=[Depends(odoo_env)], - ) - async def echo2( - message: str, - odoo_env: Annotated[Environment, Depends(odoo_env)], - ) -> EchoResponse: - """Echo the message""" - echo = odoo_env["demo.fastapi.endpoint"].echo2(message) - return EchoResponse(message=f"Echo2: {echo}") - - -In this way, the new router is added to the list of routers of your app only if -the app is called for a database where your new addon is installed. - -Extending the model used as parameter or as response of the route handler -========================================================================= - -The fastapi python library uses the pydantic library to define the models. By -default, once a model is defined, it's not possible to extend it. However, a -companion python library called -`extendable_pydantic `_ provides -a way to use inheritance with pydantic models to extend an existing model. If -used alone, it's your responsibility to instruct this library the list of -extensions to apply to a model and the order to apply them. This is not very -convenient. Fortunately, an dedicated odoo addon exists to make this process -complete transparent. This addon is called -`odoo-addon-extendable-fastapi `_. - -When you want to allow other addons to extend a pydantic model, you must -first define the model as an extendable model by using a dedicated metaclass - -.. code-block:: python - - from pydantic import BaseModel - from extendable_pydantic import ExtendableModelMeta - - class Partner(BaseModel, metaclass=ExtendableModelMeta): - name = 0.1 - model_config = ConfigDict(from_attributes=True) - -As any other pydantic model, you can now use this model as parameter or as response -of a route handler. You can also use all the features of models defined with -pydantic. - -.. code-block:: python - - @demo_api_router.get( - "/partner", - response_model=Location, - dependencies=[Depends(authenticated_partner)], - ) - async def partner( - partner: Annotated[ResPartner, Depends(authenticated_partner)], - ) -> Partner: - """Return the location""" - return Partner.model_validate(partner) - - -If you need to add a new field into the model **'Partner'**, you can extend it -in your new addon by defining a new model that inherits from the model **'Partner'**. - -.. code-block:: python - - from typing import Optional - from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner - - class PartnerExtended(Partner, extends=Partner): - email: Optional[str] - -If your new addon is installed in a database, a call to the route handler -**'/demo/partner'** will return a response with the new field **'email'** if a -value is provided by the odoo record. - -.. code-block:: python - - { - "name": "John Doe", - "email": "jhon.doe@acsone.eu" - } - -If your new addon is not installed in a database, a call to the route handler -**'/demo/partner'** will only return the name of the partner. - -.. code-block:: python - - { - "name": "John Doe" - } - -.. note:: - - The liskov substitution principle has also to be respected. That means that - if you extend a model, you must add new required fields or you must provide - default values for the new optional fields. - -Managing security into the route handlers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default the route handlers are processed using the user configured on the -**'fastapi.endpoint'** model instance. (default is the Public user). -You have seen previously how to define a dependency that will be used to enforce -the authentication of a partner. When a method depends on this dependency, the -'authenticated_partner_id' key is added to the context of the partner environment. -(If you don't need the partner as dependency but need to get an environment -with the authenticated user, you can use the dependency 'authenticated_partner_env' instead of -'authenticated_partner'.) - -The fastapi addon extends the 'ir.rule' model to add into the evaluation context -of the security rules the key 'authenticated_partner_id' that contains the id -of the authenticated partner. - -As briefly introduced in a previous section, a good practice when you develop a -fastapi app and you want to protect your data in an efficient and traceable way is to: - -* create a new user specific to the app but with any access rights. -* create a security group specific to the app and add the user to this group. (This - group must implies the group 'AFastAPI Endpoint Runner' that give the - minimal access rights) -* for each model you want to protect: - - * add a 'ir.model.access' record for the model to allow read access to your model - and add the group to the record. - * create a new 'ir.rule' record for the model that restricts the access to the - records of the model to the authenticated partner by using the key - 'authenticated_partner_id' in domain of the rule. (or to the user defined on - the 'fastapi.endpoint' model instance if the method is public) - -* add a dependency on the 'authenticated_partner' to your handlers when you need - to access the authenticated partner or ensure that the service is called by an - authenticated partner. - -.. code-block:: xml - - - My Demo Endpoint User - my_demo_app_user - - - - - My Demo Endpoint Group - - - - - - - My Demo App: access to sale.order - - - - - - - - - - - Sale Order Rule - - [('partner_id', '=', authenticated_partner_id)] - - - -How to test your fastapi app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Thanks to the starlette test client, it's possible to test your fastapi app -in a very simple way. With the test client, you can call your route handlers -as if they were real http endpoints. The test client is available in the -**'fastapi.testclient'** module. - -Once again the dependency injection mechanism comes to the rescue by allowing -you to inject into the test client specific implementations of the dependencies -normally provided by the normal processing of the request by the fastapi app. -(for example, you can inject a mock of the dependency 'authenticated_partner' -to test the behavior of your route handlers when the partner is not authenticated, -you can also inject a mock for the odoo_env etc...) - -The fastapi addon provides a base class for the test cases that you can use to -write your tests. This base class is **'odoo.fastapi.tests.common.FastAPITransactionCase'**. -This class mainly provides the method **'_create_test_client'** that you can -use to create a test client for your fastapi app. This method encapsulates the -creation of the test client and the injection of the dependencies. It also -ensures that the odoo environment is make available into the context of the -route handlers. This method is designed to be used when you need to test your -app or when you need to test a specific router (It's therefore easy to defines -tests for routers in an addon that doesn't provide a fastapi endpoint). - -With this base class, writing a test for a route handler is as simple as: - -.. code-block:: python - - from odoo.fastapi.tests.common import FastAPITransactionCase - - from odoo.addons.fastapi import dependencies - from odoo.addons.fastapi.routers import demo_router - - class FastAPIDemoCase(FastAPITransactionCase): - - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") - cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) - - def test_hello_world(self) -> None: - with self._create_test_client(router=demo_router) as test_client: - response: Response = test_client.get("/demo/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(response.json(), {"Hello": "World"}) - - -In the previous example, we created a test client for the demo_router. We could -have created a test client for the whole app by not specifying the router but -the app instead. - -.. code-block:: python - - from odoo.fastapi.tests.common import FastAPITransactionCase - - from odoo.addons.fastapi import dependencies - from odoo.addons.fastapi.routers import demo_router - - class FastAPIDemoCase(FastAPITransactionCase): - - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user") - cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"}) - - def test_hello_world(self) -> None: - demo_endpoint = self.env.ref("fastapi.fastapi_endpoint_demo") - with self._create_test_client(app=demo_endpoint._get_app()) as test_client: - response: Response = test_client.get(f"{demo_endpoint.root_path}/demo/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual(response.json(), {"Hello": "World"}) - - -Overall considerations when you develop an fastapi app -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Developing a fastapi app requires to follow some good practices to ensure that -the app is robust and easy to maintain. Here are some of them: - -* A route handler must be as simple as possible. It must not contain any - business logic. The business logic must be implemented into the service - layer. The route handler must only call the service layer and return the - result of the service layer. To ease extension on your business logic, your - service layer can be implemented as an odoo abstract model that can be - inherited by other addons. - -* A route handler should not expose the internal data structure and api of Odoo. - It should provide the api that is needed by the client. More widely, an app - provides a set of services that address a set of use cases specific to - a well defined functional domain. You must always keep in mind that your api - will remain the same for a long time even if you upgrade your odoo version - of modify your business logic. - -* A route handler is a transactional unit of work. When you design your api - you must ensure that the completeness of a use case is guaranteed by a single - transaction. If you need to perform several transactions to complete a use - case, you introduce a risk of inconsistency in your data or extra complexity - in your client code. - -* Properly handle the errors. The route handler must return a proper error - response when an error occurs. The error response must be consistent with - the rest of the api. The error response must be documented in the api - documentation. By default, the **'odoo-addon-fastapi'** module handles - the common exception types defined in the **'odoo.exceptions'** module - and returns a proper error response with the corresponding http status code. - An error in the route handler must always return an error response with a - http status code different from 200. The error response must contain a - human readable message that can be displayed to the user. The error response - can also contain a machine readable code that can be used by the client to - handle the error in a specific way. - -* When you design your json document through the pydantic models, you must - use the appropriate data types. For example, you must use the data type - **'datetime.date'** to represent a date and not a string. You must also - properly define the constraints on the fields. For example, if a field - is optional, you must use the data type **'typing.Optional'**. - `pydantic`_ provides everything you need to - properly define your json document. - -* Always use an appropriate pydantic model as request and/or response for - your route handler. Constraints on the fields of the pydantic model must - apply to the specific use case. For example, if your route handler is used - to create a sale order, the pydantic model must not contain the field - 'id' because the id of the sale order will be generated by the route handler. - But if the id is required afterwords, the pydantic model for the response - must contain the field 'id' as required. - -* Uses descriptive property names in your json documents. For example, avoid the - use of documents providing a flat list of key value pairs. - -* Be consistent in the naming of your fields into your json documents. For example, - if you use 'id' to represent the id of a sale order, you must use 'id' to represent - the id of all the other objects. - -* Be consistent in the naming style of your fields. Always prefer underscore - to camel case. - -* Always use plural for the name of the fields that contain a list of items. - For example, if you have a field 'lines' that contains a list of sale order - lines, you must use 'lines' and not 'line'. - -* You can't expect that a client will provide you the identifier of a specific - record in odoo (for example the id of a carrier) if you don't provide a - specific route handler to retrieve the list of available records. Sometimes, - the client must share with odoo the identity of a specific record to be - able to perform an appropriate action specific to this record (for example, - the processing of a payment is different for each payment acquirer). In this - case, you must provide a specific attribute that allows both the client and - odoo to identify the record. The field 'provider' on a payment acquirer allows - you to identify a specific record in odoo. This kind of approach - allows both the client and odoo to identify the record without having to rely - on the id of the record. (This will ensure that the client will not break - if the id of the record is changed in odoo for example when tests are run - on an other database). - -* Always use the same name for the same kind of object. For example, if you - have a field 'lines' that contains a list of sale order lines, you must - use the same name for the same kind of object in all the other json documents. - -* Manage relations between objects in your json documents the same way. - By default, you should return the id of the related object in the json document. - But this is not always possible or convenient, so you can also return the - related object in the json document. The main advantage of returning the id - of the related object is that it allows you to avoid the `n+1 problem - `_ . The - main advantage of returning the related object in the json document is that - it allows you to avoid an extra call to retrieve the related object. - By keeping in mind the pros and cons of each approach, you can choose the - best one for your use case. Once it's done, you must be consistent in the - way you manage the relations of the same object. - -* It's not always a good idea to name your fields into your json documents - with the same name as the fields of the corresponding odoo model. For example, - in your document representing a sale order, you must not use the name 'order_line' - for the field that contains the list of sale order lines. The name 'order_line' - in addition to being confusing and not consistent with the best practices, is - not auto-descriptive. The name 'lines' is much better. - -* Keep a defensive programming approach. If you provide a route handler that - returns a list of records, you must ensure that the computation of the list - is not too long or will not drain your server resources. For example, - for search route handlers, you must ensure that the search is limited to - a reasonable number of records by default. - -* As a corollary of the previous point, a search handler must always use the - pagination mechanism with a reasonable default page size. The result list - must be enclosed in a json document that contains the count of records into - the system matching your search criteria and the list of records for the given - page and size. - -* Use plural for the name of a service. For example, if you provide a service - that allows you to manage the sale orders, you must use the name 'sale_orders' - and not 'sale_order'. - - - -* ... and many more. - -We could write a book about the best practices to follow when you design your api -but we will stop here. This list is the result of our experience at `ACSONE SA/NV -`_ and it evolves over time. It's a kind of rescue kit that we -would provide to a new developer that starts to design an api. This kit must -be accompanied with the reading of some useful resources link like the `REST Guidelines -`_. On a technical level, -the `fastapi documentation `_ provides a lot of -useful information as well, with a lot of examples. Last but not least, the -`pydantic`_ documentation is also very useful. - -Miscellaneous -~~~~~~~~~~~~~ - -Development of a search route handler -===================================== - -The **'odoo-addon-fastapi'** module provides 2 useful piece of code to help -you be consistent when writing a route handler for a search route. - -1. A dependency method to use to specify the pagination parameters in the same - way for all the search route handlers: **'odoo.addons.fastapi.paging'**. -2. A PagedCollection pydantic model to use to return the result of a search route - handler enclosed in a json document that contains the count of records. - -.. code-block:: python - - from typing import Annotated - from pydantic import BaseModel - - from odoo.api import Environment - from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env - from odoo.addons.fastapi.schemas import PagedCollection, Paging - - class SaleOrder(BaseModel): - id: int - name: str - model_config = ConfigDict(from_attributes=True) - - - @router.get( - "/sale_orders", - response_model=PagedCollection[SaleOrder], - response_model_exclude_unset=True, - ) - def get_sale_orders( - paging: Annotated[Paging, Depends(paging)], - env: Annotated[Environment, Depends(authenticated_partner_env)], - ) -> PagedCollection[SaleOrder]: - """Get the list of sale orders.""" - count = env["sale.order"].search_count([]) - orders = env["sale.order"].search([], limit=paging.limit, offset=paging.offset) - return PagedCollection[SaleOrder]( - count=count, - items=[SaleOrder.model_validate(order) for order in orders], - ) - -.. note:: - - The **'odoo.addons.fastapi.schemas.Paging'** and **'odoo.addons.fastapi.schemas.PagedCollection'** - pydantic models are not designed to be extended to not introduce a - dependency between the **'odoo-addon-fastapi'** module and the **'odoo-addon-extendable'** - - -Customization of the error handling -=================================== - -The error handling a very important topic in the design of the fastapi integration -with odoo. It must ensure that the error messages are properly return to the client -and that the transaction is properly roll backed. The **'fastapi'** module provides -a way to register custom error handlers. The **'odoo.addons.fastapi.error_handlers'** -module provides the default error handlers that are registered by default when -a new instance of the **'FastAPI'** class is created. When an app is initialized in -'fastapi.endpoint' model, the method `_get_app_exception_handlers` is called to -get a dictionary of error handlers. This method is designed to be overridden -in a custom module to provide custom error handlers. You can override the handler -for a specific exception class or you can add a new handler for a new exception -or even replace all the handlers by your own handlers. Whatever you do, you must -ensure that the transaction is properly roll backed. - -Some could argue that the error handling can't be extended since the error handlers -are global method not defined in an odoo model. Since the method providing the -the error handlers definitions is defined on the 'fastapi.endpoint' model, it's -not a problem at all, you just need to think another way to do it that by inheritance. - -A solution could be to develop you own error handler to be able to process the -error and chain the call to the default error handler. - -.. code-block:: python - - class MyCustomErrorHandler(): - def __init__(self, next_handler): - self.next_handler = next_handler - - def __call__(self, request: Request, exc: Exception) -> JSONResponse: - # do something with the error - response = self.next_handler(request, exc) - # do something with the response - return response - - -With this solution, you can now register your custom error handler by overriding -the method `_get_app_exception_handlers` in your custom module. - -.. code-block:: python - - class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" - - def _get_app_exception_handlers( - self, - ) -> Dict[ - Union[int, Type[Exception]], - Callable[[Request, Exception], Union[Response, Awaitable[Response]]], - ]: - handlers = super()._get_app_exception_handlers() - access_error_handler = handlers.get(odoo.exceptions.AccessError) - handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler) - return handlers - -In the previous example, we extend the error handler for the 'AccessError' exception -for all the endpoints. You can do the same for a specific app by checking the -'app' field of the 'fastapi.endpoint' record before registering your custom error -handler. - -FastAPI addons directory structure -================================== - -When you develop a new addon to expose an api with fastapi, it's a good practice -to follow the same directory structure and naming convention for the files -related to the api. It will help you to easily find the files related to the api -and it will help the other developers to understand your code. - -Here is the directory structure that we recommend. It's based on practices that -are used in the python community when developing a fastapi app. - -.. code-block:: - - . - ├── x_api - │   ├── data - │   │ ├── ... .xml - │   ├── demo - │   │ ├── ... .xml - │   ├── i18n - │   │ ├── ... .po - │   ├── models - │   │ ├── __init__.py - │   │ ├── fastapi_endpoint.py # your app - │   │ └── ... .py - │   └── routers - │   │ ├── __init__.py - │   │ ├── items.py - │   │ └── ... .py - │   ├── schemas | schemas.py - │   │ ├── __init__.py - │   │ ├── my_model.py # pydantic model - │   │ └── ... .py - │   ├── security - │   │ ├── ... .xml - │   ├── views - │   │ ├── ... .xml - │   ├── __init__.py - │   ├── __manifest__.py - │   ├── dependencies.py # custom dependencies - │   ├── error_handlers.py # custom error handlers - - -* The **'models'** directory contains the odoo models. When you define a new - app, as for the others addons, you will add your new model inheriting from - the **'fastapi.endpoint'** model in this directory. -* The **'routers'** directory contains the fastapi routers. You will add your - new routers in this directory. Each route starting with the same prefix should - be grouped in the same file. For example, all the routes starting with - '/items' should be defined in the **'items.py'** file. The **'__init__.py'** - file in this directory is used to import all the routers defined in the - directory and create a global router that can be used in an app. For example, - in your **'items.py'** file, you will define a router like this: - - .. code-block:: python - - router = APIRouter(tags=["items"]) - - router.get("/items", response_model=List[Item]) - def list_items(): - pass - - In the **'__init__.py'** file, you will import the router and add it to the global - router or your addon. - - .. code-block:: python - - from fastapi import APIRouter - - from .items import router as items_router - - router = APIRouter() - router.include_router(items_router) - -* The **'schemas.py'** will be used to define the pydantic models. For complex - APIs with a lot of models, it will be better to create a **'schemas'** directory - and split the models in different files. The **'__init__.py'** file in this - directory will be used to import all the models defined in the directory. - For example, in your **'my_model.py'** - file, you will define a model like this: - - .. code-block:: python - - from pydantic import BaseModel - - class MyModel(BaseModel): - name: str - description: str = None - - In the **'__init__.py'** file, you will import the model's classes from the - files in the directory. - - .. code-block:: python - - from .my_model import MyModel - - This will allow to always import the models from the schemas module whatever - the models are spread across different files or defined in the **'schemas.py'** - file. - - .. code-block:: python - - from x_api_addon.schemas import MyModel - -* The **'dependencies.py'** file contains the custom dependencies that you - will use in your routers. For example, you can define a dependency to - check the access rights of the user. -* The **'error_handlers.py'** file contains the custom error handlers that you - will use in your routers. The **'odoo-addon-fastapi'** module provides the - default error handlers for the common odoo exceptions. Chance are that you - will not need to define your own error handlers. But if you need to do it, - you can define them in this file. - -What's next? -~~~~~~~~~~~~ - -The **'odoo-addon-fastapi'** module is still in its early stage of development. -It will evolve over time to integrate your feedback and to provide the missing -features. It's now up to you to try it and to provide your feedback. - -.. _pydantic: https://docs.pydantic.dev/ diff --git a/fastapi/schemas.py b/fastapi/schemas.py index 9d3835ae..b8713f0a 100644 --- a/fastapi/schemas.py +++ b/fastapi/schemas.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import warnings from enum import Enum -from typing import Annotated, Generic, List, Optional, TypeVar +from typing import Annotated, Generic, Optional, TypeVar from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field @@ -19,7 +19,7 @@ class PagedCollection(BaseModel, Generic[T]): validation_alias=AliasChoices("count", "total"), ), ] - items: List[T] + items: list[T] @computed_field() @property @@ -37,8 +37,8 @@ def total(self, value: int): class Paging(BaseModel): - limit: Optional[int] = None - offset: Optional[int] = None + limit: Optional[int] = None # noqa: UP007 + offset: Optional[int] = None # noqa: UP007 ############################################################# diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 6d439c4d..fd61c88b 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -1,4 +1,3 @@ - @@ -369,38 +368,43 @@

    Odoo FastAPI

    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:de5a0e0642df22387984fd10d2e54007b88126bacdb22e9e8b637c6187abaeeb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    -

    This addon provides the basis to smoothly integrate the FastAPI -framework into Odoo.

    -

    This integration allows you to use all the goodies from FastAPI to build custom -APIs for your Odoo server based on standard Python type hints.

    +

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This addon provides the basis to smoothly integrate the +FastAPI framework into Odoo.

    +

    This integration allows you to use all the goodies from +FastAPI to build custom APIs for +your Odoo server based on standard Python type hints.

    What is building an API?

    -

    An API is a set of functions that can be called from the outside world. The -goal of an API is to provide a way to interact with your application from the -outside world without having to know how it works internally. A common mistake -when you are building an API is to expose all the internal functions of your -application and therefore create a tight coupling between the outside world and -your internal datamodel and business logic. This is not a good idea because it -makes it very hard to change your internal datamodel and business logic without -breaking the outside world.

    -

    When you are building an API, you define a contract between the outside world -and your application. This contract is defined by the functions that you expose -and the parameters that you accept. This contract is the API. When you change -your internal datamodel and business logic, you can still keep the same API -contract and therefore you don’t break the outside world. Even if you change -your implementation, as long as you keep the same API contract, the outside -world will still work. This is the beauty of an API and this is why it is so -important to design a good API.

    -

    A good API is designed to be stable and to be easy to use. It’s designed to -provide high-level functions related to a specific use case. It’s designed to -be easy to use by hiding the complexity of the internal datamodel and business -logic. A common mistake when you are building an API is to expose all the internal -functions of your application and let the oustide world deal with the complexity -of your internal datamodel and business logic. Don’t forget that on a transactional -point of view, each call to an API function is a transaction. This means that -if a specific use case requires multiple calls to your API, you should provide -a single function that does all the work in a single transaction. This why APIs -methods are called high-level and atomic functions.

    +

    An API is a set of functions that can be called from the outside world. +The goal of an API is to provide a way to interact with your application +from the outside world without having to know how it works internally. A +common mistake when you are building an API is to expose all the +internal functions of your application and therefore create a tight +coupling between the outside world and your internal datamodel and +business logic. This is not a good idea because it makes it very hard to +change your internal datamodel and business logic without breaking the +outside world.

    +

    When you are building an API, you define a contract between the outside +world and your application. This contract is defined by the functions +that you expose and the parameters that you accept. This contract is the +API. When you change your internal datamodel and business logic, you can +still keep the same API contract and therefore you don’t break the +outside world. Even if you change your implementation, as long as you +keep the same API contract, the outside world will still work. This is +the beauty of an API and this is why it is so important to design a good +API.

    +

    A good API is designed to be stable and to be easy to use. It’s designed +to provide high-level functions related to a specific use case. It’s +designed to be easy to use by hiding the complexity of the internal +datamodel and business logic. A common mistake when you are building an +API is to expose all the internal functions of your application and let +the oustide world deal with the complexity of your internal datamodel +and business logic. Don’t forget that on a transactional point of view, +each call to an API function is a transaction. This means that if a +specific use case requires multiple calls to your API, you should +provide a single function that does all the work in a single +transaction. This why APIs methods are called high-level and atomic +functions.

    Table of contents

  • +
  • How to extend an existing app -
  • +
  • Miscellaneous
  • @@ -452,54 +456,60 @@

    Odoo FastAPI

    Usage

    What’s building an API with fastapi?

    -

    FastAPI is a modern, fast (high-performance), web framework for building APIs -with Python 3.7+ based on standard Python type hints. This addons let’s you -keep advantage of the fastapi framework and use it with Odoo.

    +

    FastAPI is a modern, fast (high-performance), web framework for building +APIs with Python 3.7+ based on standard Python type hints. This addons +let’s you keep advantage of the fastapi framework and use it with Odoo.

    Before you start, we must define some terms:

    The FastAPI framework is based on the following principles:

    The first step is to install the fastapi addon. You can do it with the following command:

    $ pip install odoo-addon-fastapi
    -

    Once the addon is installed, you can start building your API. The first thing -you need to do is to create a new addon that depends on ‘fastapi’. For example, -let’s create an addon called my_demo_api.

    -

    Then, you need to declare your app by defining a model that inherits from -‘fastapi.endpoint’ and add your app name into the app field. For example:

    +

    Once the addon is installed, you can start building your API. The first +thing you need to do is to create a new addon that depends on ‘fastapi’. +For example, let’s create an addon called my_demo_api.

    +

    Then, you need to declare your app by defining a model that inherits +from ‘fastapi.endpoint’ and add your app name into the app field. For +example:

     from odoo import fields, models
     
    @@ -511,20 +521,23 @@ 

    What’s building an API with fas selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} )

    -

    The ‘fastapi.endpoint’ model is the base model for all the endpoints. An endpoint -instance is the mount point for a fastapi app into Odoo. When you create a new -endpoint, you can define the app that you want to mount in the ‘app’ field -and the path where you want to mount it in the ‘path’ field.

    +

    The ‘fastapi.endpoint’ model is the base model for all the +endpoints. An endpoint instance is the mount point for a fastapi app +into Odoo. When you create a new endpoint, you can define the app that +you want to mount in the ‘app’ field and the path where you want to +mount it in the ‘path’ field.

    figure:: static/description/endpoint_create.png

    FastAPI Endpoint
    -

    Thanks to the ‘fastapi.endpoint’ model, you can create as many endpoints as -you want and mount as many apps as you want in each endpoint. The endpoint is -also the place where you can define configuration parameters for your app. A -typical example is the authentication method that you want to use for your app -when accessed at the endpoint path.

    -

    Now, you can create your first router. For that, you need to define a global -variable into your fastapi_endpoint module called for example ‘demo_api_router’

    +

    Thanks to the ‘fastapi.endpoint’ model, you can create as many +endpoints as you want and mount as many apps as you want in each +endpoint. The endpoint is also the place where you can define +configuration parameters for your app. A typical example is the +authentication method that you want to use for your app when accessed at +the endpoint path.

    +

    Now, you can create your first router. For that, you need to define a +global variable into your fastapi_endpoint module called for example +‘demo_api_router’

     from fastapi import APIRouter
     from odoo import fields, models
    @@ -540,8 +553,9 @@ 

    What’s building an API with fas # create a router demo_api_router = APIRouter()

    -

    To make your router available to your app, you need to add it to the list of routers -returned by the _get_fastapi_routers method of your fastapi_endpoint model.

    +

    To make your router available to your app, you need to add it to the +list of routers returned by the _get_fastapi_routers method of your +fastapi_endpoint model.

     from fastapi import APIRouter
     from odoo import api, fields, models
    @@ -562,8 +576,8 @@ 

    What’s building an API with fas # create a router demo_api_router = APIRouter()

    -

    Now, you can start adding routes to your router. For example, let’s add a route -that returns a list of partners.

    +

    Now, you can start adding routes to your router. For example, let’s add +a route that returns a list of partners.

     from typing import Annotated
     
    @@ -602,17 +616,18 @@ 

    What’s building an API with fas for partner in env["res.partner"].search([]) ]

    -

    Now, you can start your Odoo server, install your addon and create a new endpoint -instance for your app. Once it’s done click on the docs url to access the -interactive documentation of your app.

    -

    Before trying to test your app, you need to define on the endpoint instance the -user that will be used to run the app. You can do it by setting the ‘user_id’ -field. This information is the most important one because it’s the basis for -the security of your app. The user that you define in the endpoint instance -will be used to run the app and to access the database. This means that the -user will be able to access all the data that he has access to in Odoo. To ensure -the security of your app, you should create a new user that will be used only -to run your app and that will have no access to the database.

    +

    Now, you can start your Odoo server, install your addon and create a new +endpoint instance for your app. Once it’s done click on the docs url to +access the interactive documentation of your app.

    +

    Before trying to test your app, you need to define on the endpoint +instance the user that will be used to run the app. You can do it by +setting the ‘user_id’ field. This information is the most important +one because it’s the basis for the security of your app. The user that +you define in the endpoint instance will be used to run the app and to +access the database. This means that the user will be able to access all +the data that he has access to in Odoo. To ensure the security of your +app, you should create a new user that will be used only to run your app +and that will have no access to the database.

     <record
           id="my_demo_app_user"
    @@ -624,10 +639,10 @@ 

    What’s building an API with fas <field name="groups_id" eval="[(6, 0, [])]" /> </record>

    -

    At the same time you should create a new group that will be used to define the -access rights of the user that will run your app. This group should imply -the predefined group ‘FastAPI Endpoint Runner’. This group defines the -minimum access rights that the user needs to:

    +

    At the same time you should create a new group that will be used to +define the access rights of the user that will run your app. This group +should imply the predefined group ‘FastAPI Endpoint Runner’. This +group defines the minimum access rights that the user needs to:

    Dealing with the odoo environment

    -

    The ‘odoo.addons.fastapi.dependencies’ module provides a set of functions that you can use -to inject reusable dependencies into your routes. For example, the ‘odoo_env’ -function returns the current odoo environment. You can use it to access the -odoo models and the database from your route handlers.

    +

    The ‘odoo.addons.fastapi.dependencies’ module provides a set of +functions that you can use to inject reusable dependencies into your +routes. For example, the ‘odoo_env’ function returns the current +odoo environment. You can use it to access the odoo models and the +database from your route handlers.

     from typing import Annotated
     
    @@ -673,47 +690,50 @@ 

    Dealing with the odoo environment for partner in env["res.partner"].search([]) ]

    -

    As you can see, you can use the ‘Depends’ function to inject the dependency -into your route handler. The ‘Depends’ function is provided by the -‘fastapi’ framework. You can use it to inject any dependency into your route -handler. As your handler is a python function, the only way to get access to -the odoo environment is to inject it as a dependency. The fastapi addon provides -a set of function that can be used as dependencies:

    +

    As you can see, you can use the ‘Depends’ function to inject the +dependency into your route handler. The ‘Depends’ function is +provided by the ‘fastapi’ framework. You can use it to inject any +dependency into your route handler. As your handler is a python +function, the only way to get access to the odoo environment is to +inject it as a dependency. The fastapi addon provides a set of function +that can be used as dependencies:

    • ‘odoo_env’: Returns the current odoo environment.
    • -
    • ‘fastapi_endpoint’: Returns the current fastapi endpoint model instance.
    • +
    • ‘fastapi_endpoint’: Returns the current fastapi endpoint model +instance.
    • ‘authenticated_partner’: Returns the authenticated partner.
    • -
    • ‘authenticated_partner_env’: Returns the current odoo environment with the -authenticated_partner_id into the context.
    • +
    • ‘authenticated_partner_env’: Returns the current odoo environment +with the authenticated_partner_id into the context.
    -

    By default, the ‘odoo_env’ and ‘fastapi_endpoint’ dependencies are -available without extra work.

    -
    -

    Note

    -

    Even if ‘odoo_env’ and ‘authenticated_partner_env’ returns the current odoo -environment, they are not the same. The ‘odoo_env’ dependency returns the -environment without any modification while the ‘authenticated_partner_env’ -adds the authenticated partner id into the context of the environment. As it will -be explained in the section Managing security into the route handlers dedicated -to the security, the presence of the authenticated partner id into the context -is the key information that will allow you to enforce the security of your endpoint -methods. As consequence, you should always use the ‘authenticated_partner_env’ -dependency instead of the ‘odoo_env’ dependency for all the methods that are -not public.

    -
    +

    By default, the ‘odoo_env’ and ‘fastapi_endpoint’ dependencies +are available without extra work.

    +

    Note

    +

    Even if ‘odoo_env’ and ‘authenticated_partner_env’ returns the current +odoo environment, they are not the same. The ‘odoo_env’ dependency +returns the environment without any modification while the +‘authenticated_partner_env’ adds the authenticated partner id into the +context of the environment. As it will be explained in the section +Managing security into the route +handlers dedicated to +the security, the presence of the authenticated partner id into the +context is the key information that will allow you to enforce the +security of your endpoint methods. As consequence, you should always use +the ‘authenticated_partner_env’ dependency instead of the ‘odoo_env’ +dependency for all the methods that are not public.

    The dependency injection mechanism

    -

    The ‘odoo_env’ dependency relies on a simple implementation that retrieves -the current odoo environment from ContextVar variable initialized at the start -of the request processing by the specific request dispatcher processing the -fastapi requests.

    -

    The ‘fastapi_endpoint’ dependency relies on the ‘dependency_overrides’ mechanism -provided by the ‘fastapi’ module. (see the fastapi documentation for more -details about the dependency_overrides mechanism). If you take a look at the -current implementation of the ‘fastapi_endpoint’ dependency, you will see -that the method depends of two parameters: ‘endpoint_id’ and ‘env’. Each -of these parameters are dependencies themselves.

    +

    The ‘odoo_env’ dependency relies on a simple implementation that +retrieves the current odoo environment from ContextVar variable +initialized at the start of the request processing by the specific +request dispatcher processing the fastapi requests.

    +

    The ‘fastapi_endpoint’ dependency relies on the +‘dependency_overrides’ mechanism provided by the ‘fastapi’ module. +(see the fastapi documentation for more details about the +dependency_overrides mechanism). If you take a look at the current +implementation of the ‘fastapi_endpoint’ dependency, you will see +that the method depends of two parameters: ‘endpoint_id’ and +‘env’. Each of these parameters are dependencies themselves.

     def fastapi_endpoint_id() -> int:
         """This method is overriden by default to make the fastapi.endpoint record
    @@ -730,13 +750,15 @@ 

    The dependency injection mechanis """Return the fastapi.endpoint record""" return env["fastapi.endpoint"].browse(_id)

    -

    As you can see, one of these dependencies is the ‘fastapi_endpoint_id’ -dependency and has no concrete implementation. This method is used as a contract -that must be implemented/provided at the time the fastapi app is created. -Here comes the power of the dependency_overrides mechanism.

    -

    If you take a look at the ‘_get_app’ method of the ‘FastapiEndpoint’ model, -you will see that the ‘fastapi_endpoint_id’ dependency is overriden by -registering a specific method that returns the id of the current fastapi endpoint +

    As you can see, one of these dependencies is the +‘fastapi_endpoint_id’ dependency and has no concrete implementation. +This method is used as a contract that must be implemented/provided at +the time the fastapi app is created. Here comes the power of the +dependency_overrides mechanism.

    +

    If you take a look at the ‘_get_app’ method of the +‘FastapiEndpoint’ model, you will see that the +‘fastapi_endpoint_id’ dependency is overriden by registering a +specific method that returns the id of the current fastapi endpoint model instance for the original method.

     def _get_app(self) -> FastAPI:
    @@ -747,18 +769,20 @@ 

    The dependency injection mechanis lambda a: a, self.id )

    -

    This kind of mechanism is very powerful and allows you to inject any dependency -into your route handlers and moreover, define an abstract dependency that can be -used by any other addon and for which the implementation could depend on the -endpoint configuration.

    +

    This kind of mechanism is very powerful and allows you to inject any +dependency into your route handlers and moreover, define an abstract +dependency that can be used by any other addon and for which the +implementation could depend on the endpoint configuration.

    The authentication mechanism

    -

    To make our app not tightly coupled with a specific authentication mechanism, -we will use the ‘authenticated_partner’ dependency. As for the -‘fastapi_endpoint’ this dependency depends on an abstract dependency.

    -

    When you define a route handler, you can inject the ‘authenticated_partner’ -dependency as a parameter of your route handler.

    +

    To make our app not tightly coupled with a specific authentication +mechanism, we will use the ‘authenticated_partner’ dependency. As +for the ‘fastapi_endpoint’ this dependency depends on an abstract +dependency.

    +

    When you define a route handler, you can inject the +‘authenticated_partner’ dependency as a parameter of your route +handler.

     from odoo.addons.base.models.res_partner import Partner
     
    @@ -772,13 +796,14 @@ 

    The authentication mechanism< for partner in env["res.partner"].search([]) ]

    -

    At this stage, your handler is not tied to a specific authentication mechanism -but only expects to get a partner as a dependency. Depending on your needs, you -can implement different authentication mechanism available for your app. -The fastapi addon provides a default authentication mechanism using the -‘BasicAuth’ method. This authentication mechanism is implemented in the -‘odoo.addons.fastapi.dependencies’ module and relies on functionalities provided -by the ‘fastapi.security’ module.

    +

    At this stage, your handler is not tied to a specific authentication +mechanism but only expects to get a partner as a dependency. Depending +on your needs, you can implement different authentication mechanism +available for your app. The fastapi addon provides a default +authentication mechanism using the ‘BasicAuth’ method. This +authentication mechanism is implemented in the +‘odoo.addons.fastapi.dependencies’ module and relies on +functionalities provided by the ‘fastapi.security’ module.

     def authenticated_partner(
         env: Annotated[Environment, Depends(odoo_env)],
    @@ -803,20 +828,22 @@ 

    The authentication mechanism< return partner

    As you can see, the ‘authenticated_partner’ dependency relies on the -‘HTTPBasic’ dependency provided by the ‘fastapi.security’ module. -In this dummy implementation, we just check that the provided credentials -can be used to authenticate a user in odoo. If the authentication is successful, -we return the partner record linked to the authenticated user.

    -

    In some cases you could want to implement a more complex authentication mechanism -that could rely on a token or a session. In this case, you can override the -‘authenticated_partner’ dependency by registering a specific method that -returns the authenticated partner. Moreover, you can make it configurable on -the fastapi endpoint model instance.

    +‘HTTPBasic’ dependency provided by the ‘fastapi.security’ +module. In this dummy implementation, we just check that the provided +credentials can be used to authenticate a user in odoo. If the +authentication is successful, we return the partner record linked to the +authenticated user.

    +

    In some cases you could want to implement a more complex authentication +mechanism that could rely on a token or a session. In this case, you can +override the ‘authenticated_partner’ dependency by registering a +specific method that returns the authenticated partner. Moreover, you +can make it configurable on the fastapi endpoint model instance.

    To do it, you just need to implement a specific method for each of your -authentication mechanism and allows the user to select one of these methods -when he creates a new fastapi endpoint. Let’s say that we want to allow the -authentication by using an api key or via basic auth. Since basic auth is already -implemented, we will only implement the api key authentication mechanism.

    +authentication mechanism and allows the user to select one of these +methods when he creates a new fastapi endpoint. Let’s say that we want +to allow the authentication by using an api key or via basic auth. Since +basic auth is already implemented, we will only implement the api key +authentication mechanism.

     from fastapi.security import APIKeyHeader
     
    @@ -839,11 +866,12 @@ 

    The authentication mechanism< ) return partner

    -

    As for the ‘BasicAuth’ authentication mechanism, we also rely on one of the native -security dependency provided by the ‘fastapi.security’ module.

    -

    Now that we have an implementation for our two authentication mechanisms, we -can allows the user to select one of these authentication mechanisms by adding -a selection field on the fastapi endpoint model.

    +

    As for the ‘BasicAuth’ authentication mechanism, we also rely on one of +the native security dependency provided by the ‘fastapi.security’ +module.

    +

    Now that we have an implementation for our two authentication +mechanisms, we can allows the user to select one of these authentication +mechanisms by adding a selection field on the fastapi endpoint model.

     from odoo import fields, models
     
    @@ -859,16 +887,14 @@ 

    The authentication mechanism< string="Authenciation method", )

    -
    -

    Note

    -

    A good practice is to prefix specific configuration fields of your app with -the name of your app. This will avoid conflicts with other app when the -‘fastapi.endpoint’ model is extended for other ‘app’.

    -
    +

    Note

    +

    A good practice is to prefix specific configuration fields of your app +with the name of your app. This will avoid conflicts with other app when +the ‘fastapi.endpoint’ model is extended for other ‘app’.

    Now that we have a selection field that allows the user to select the authentication method, we can use the dependency override mechanism to -provide the right implementation of the ‘authenticated_partner’ dependency -when the app is instantiated.

    +provide the right implementation of the ‘authenticated_partner’ +dependency when the app is instantiated.

     from odoo.addons.fastapi.dependencies import authenticated_partner
     class FastapiEndpoint(models.Model):
    @@ -901,25 +927,26 @@ 

    The authentication mechanism< ] = authenticated_partner_impl_override return app

    -

    To see how the dependency override mechanism works, you can take a look at the -demo app provided by the fastapi addon. If you choose the app ‘demo’ in the -fastapi endpoint form view, you will see that the authentication method -is configurable. You can also see that depending on the authentication method -configured on your fastapi endpoint, the documentation will change.

    -
    -

    Note

    -

    At time of writing, the dependency override mechanism is not supported by -the fastapi documentation generator. A fix has been proposed and is waiting -to be merged. You can follow the progress of the fix on github

    -
    +

    To see how the dependency override mechanism works, you can take a look +at the demo app provided by the fastapi addon. If you choose the app +‘demo’ in the fastapi endpoint form view, you will see that the +authentication method is configurable. You can also see that depending +on the authentication method configured on your fastapi endpoint, the +documentation will change.

    +

    Note

    +

    At time of writing, the dependency override mechanism is not supported +by the fastapi documentation generator. A fix has been proposed and is +waiting to be merged. You can follow the progress of the fix on +github

    Managing configuration parameters for your app

    -

    As we have seen in the previous section, you can add configuration fields -on the fastapi endpoint model to allow the user to configure your app (as for -any odoo model you extend). When you need to access these configuration fields -in your route handlers, you can use the ‘odoo.addons.fastapi.dependencies.fastapi_endpoint’ -dependency method to retrieve the ‘fastapi.endpoint’ record associated to the +

    As we have seen in the previous section, you can add configuration +fields on the fastapi endpoint model to allow the user to configure your +app (as for any odoo model you extend). When you need to access these +configuration fields in your route handlers, you can use the +‘odoo.addons.fastapi.dependencies.fastapi_endpoint’ dependency +method to retrieve the ‘fastapi.endpoint’ record associated to the current request.

     from pydantic import BaseModel, Field
    @@ -948,14 +975,15 @@ 

    Managing configuration parameters # even if the method doesn't require the authenticated partner as parameter return EndpointAppInfo.model_validate(endpoint)

    -

    Some of the configuration fields of the fastapi endpoint could impact the way -the app is instantiated. For example, in the previous section, we have seen -that the authentication method configured on the ‘fastapi.endpoint’ record is -used in order to provide the right implementation of the ‘authenticated_partner’ -when the app is instantiated. To ensure that the app is re-instantiated when -an element of the configuration used in the instantiation of the app is -modified, you must override the ‘_fastapi_app_fields’ method to add the -name of the fields that impact the instantiation of the app into the returned +

    Some of the configuration fields of the fastapi endpoint could impact +the way the app is instantiated. For example, in the previous section, +we have seen that the authentication method configured on the +‘fastapi.endpoint’ record is used in order to provide the right +implementation of the ‘authenticated_partner’ when the app is +instantiated. To ensure that the app is re-instantiated when an element +of the configuration used in the instantiation of the app is modified, +you must override the ‘_fastapi_app_fields’ method to add the name +of the fields that impact the instantiation of the app into the returned list.

     class FastapiEndpoint(models.Model):
    @@ -979,57 +1007,61 @@ 

    Managing configuration parameters

    Dealing with languages

    -

    The fastapi addon parses the Accept-Language header of the request to determine -the language to use. This parsing is done by respecting the RFC 7231 specification. That means that -the language is determined by the first language found in the header that is -supported by odoo (with care of the priority order). If no language is found in -the header, the odoo default language is used. This language is then used to -initialize the Odoo’s environment context used by the route handlers. All this -makes the management of languages very easy. You don’t have to worry about. This -feature is also documented by default into the generated openapi documentation -of your app to instruct the api consumers how to request a specific language.

    +

    The fastapi addon parses the Accept-Language header of the request to +determine the language to use. This parsing is done by respecting the +RFC 7231 +specification. +That means that the language is determined by the first language found +in the header that is supported by odoo (with care of the priority +order). If no language is found in the header, the odoo default language +is used. This language is then used to initialize the Odoo’s environment +context used by the route handlers. All this makes the management of +languages very easy. You don’t have to worry about. This feature is also +documented by default into the generated openapi documentation of your +app to instruct the api consumers how to request a specific language.

    How to extend an existing app

    When you develop a fastapi app, in a native python app it’s not possible -to extend an existing one. This limitation doesn’t apply to the fastapi addon -because the fastapi endpoint model is designed to be extended. However, the -way to extend an existing app is not the same as the way to extend an odoo model.

    -

    First of all, it’s important to keep in mind that when you define a route, you -are actually defining a contract between the client and the server. This -contract is defined by the route path, the method (GET, POST, PUT, DELETE, -etc.), the parameters and the response. If you want to extend an existing app, -you must ensure that the contract is not broken. Any change to the contract -will respect the Liskov substitution principle. This means -that the client should not be impacted by the change.

    -

    What does it mean in practice? It means that you can’t change the route path -or the method of an existing route. You can’t change the name of a parameter -or the type of a response. You can’t add a new parameter or a new response. -You can’t remove a parameter or a response. If you want to change the contract, -you must create a new route.

    +to extend an existing one. This limitation doesn’t apply to the fastapi +addon because the fastapi endpoint model is designed to be extended. +However, the way to extend an existing app is not the same as the way to +extend an odoo model.

    +

    First of all, it’s important to keep in mind that when you define a +route, you are actually defining a contract between the client and the +server. This contract is defined by the route path, the method (GET, +POST, PUT, DELETE, etc.), the parameters and the response. If you want +to extend an existing app, you must ensure that the contract is not +broken. Any change to the contract will respect the Liskov substitution +principle. +This means that the client should not be impacted by the change.

    +

    What does it mean in practice? It means that you can’t change the route +path or the method of an existing route. You can’t change the name of a +parameter or the type of a response. You can’t add a new parameter or a +new response. You can’t remove a parameter or a response. If you want to +change the contract, you must create a new route.

    What can you change?

    Let’s see how to do that.

    -
    -
    -

    Changing the implementation of the route handler

    -

    Let’s say that you want to change the implementation of the route handler -‘/demo/echo’. Since a route handler is just a python method, it could seems -a tedious task since we are not into a model method and therefore we can’t -take advantage of the Odoo inheritance mechanism.

    -

    However, the fastapi addon provides a way to do that. Thanks to the ‘odoo_env’ -dependency method, you can access the current odoo environment. With this -environment, you can access the registry and therefore the model you want to -delegate the implementation to. If you want to change the implementation of -the route handler ‘/demo/echo’, the only thing you have to do is to -inherit from the model where the implementation is defined and override the -method ‘echo’.

    +

    Changing the implementation of the route handler

    +

    Let’s say that you want to change the implementation of the route +handler ‘/demo/echo’. Since a route handler is just a python method, +it could seems a tedious task since we are not into a model method and +therefore we can’t take advantage of the Odoo inheritance mechanism.

    +

    However, the fastapi addon provides a way to do that. Thanks to the +‘odoo_env’ dependency method, you can access the current odoo +environment. With this environment, you can access the registry and +therefore the model you want to delegate the implementation to. If you +want to change the implementation of the route handler ‘/demo/echo’, +the only thing you have to do is to inherit from the model where the +implementation is defined and override the method ‘echo’.

     from pydantic import BaseModel
     from fastapi import Depends, APIRouter
    @@ -1077,33 +1109,33 @@ 

    Changing the implementation of th def echo(self, message: str) -> str: return f"Hello {message}"

    -
    -

    Note

    -

    It’s a good programming practice to implement the business logic outside -the route handler. This way, you can easily test your business logic without -having to test the route handler. In the example above, the business logic -is implemented in the method ‘echo’ of the model ‘demo.fastapi.endpoint’. -The route handler just delegate the implementation to this method.

    -
    +

    Note

    +

    It’s a good programming practice to implement the business logic outside +the route handler. This way, you can easily test your business logic +without having to test the route handler. In the example above, the +business logic is implemented in the method ‘echo’ of the model +‘demo.fastapi.endpoint’. The route handler just delegate the +implementation to this method.

    -

    Overriding the dependencies of the route handler

    -

    As you’ve previously seen, the dependency injection mechanism of fastapi is -very powerful. By designing your route handler to rely on dependencies with -a specific functional scope, you can easily change the implementation of the -dependency without having to change the route handler. With such a design, you -can even define abstract dependencies that must be implemented by the concrete -application. This is the case of the ‘authenticated_partner’ dependency in our -previous example. (you can find the implementation of this dependency in the -file ‘odoo/addons/fastapi/dependencies.py’ and it’s usage in the file +

    Overriding the dependencies of the route handler

    +

    As you’ve previously seen, the dependency injection mechanism of fastapi +is very powerful. By designing your route handler to rely on +dependencies with a specific functional scope, you can easily change the +implementation of the dependency without having to change the route +handler. With such a design, you can even define abstract dependencies +that must be implemented by the concrete application. This is the case +of the ‘authenticated_partner’ dependency in our previous example. +(you can find the implementation of this dependency in the file +‘odoo/addons/fastapi/dependencies.py’ and it’s usage in the file ‘odoo/addons/fastapi/models/fastapi_endpoint_demo.py’)

    -

    Adding a new route handler

    +

    Adding a new route handler

    Let’s say that you want to add a new route handler ‘/demo/echo2’. You could be tempted to add this new route handler in your new addons by -importing the router of the existing app and adding the new route handler to -it.

    +importing the router of the existing app and adding the new route +handler to it.

     from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router
     
    @@ -1120,11 +1152,11 @@ 

    Adding a new route handler echo = odoo_env["demo.fastapi.endpoint"].echo2(message) return EchoResponse(message=f"Echo2: {echo}")

    -

    The problem with this approach is that you unconditionally add the new route -handler to the existing app even if the app is called for a different database -where your new addon is not installed.

    -

    The solution is to define a new router and to add it to the list of routers -returned by the method ‘_get_fastapi_routers’ of the model +

    The problem with this approach is that you unconditionally add the new +route handler to the existing app even if the app is called for a +different database where your new addon is not installed.

    +

    The solution is to define a new router and to add it to the list of +routers returned by the method ‘_get_fastapi_routers’ of the model ‘fastapi.endpoint’ you are inheriting from into your new addon.

     class FastapiEndpoint(models.Model):
    @@ -1152,23 +1184,26 @@ 

    Adding a new route handler echo = odoo_env["demo.fastapi.endpoint"].echo2(message) return EchoResponse(message=f"Echo2: {echo}")

    -

    In this way, the new router is added to the list of routers of your app only if -the app is called for a database where your new addon is installed.

    +

    In this way, the new router is added to the list of routers of your app +only if the app is called for a database where your new addon is +installed.

    -

    Extending the model used as parameter or as response of the route handler

    -

    The fastapi python library uses the pydantic library to define the models. By -default, once a model is defined, it’s not possible to extend it. However, a -companion python library called -extendable_pydantic provides -a way to use inheritance with pydantic models to extend an existing model. If -used alone, it’s your responsibility to instruct this library the list of -extensions to apply to a model and the order to apply them. This is not very -convenient. Fortunately, an dedicated odoo addon exists to make this process -complete transparent. This addon is called +

    Extending the model used as parameter or as response of the route handler

    +

    The fastapi python library uses the pydantic library to define the +models. By default, once a model is defined, it’s not possible to extend +it. However, a companion python library called +extendable_pydantic +provides a way to use inheritance with pydantic models to extend an +existing model. If used alone, it’s your responsibility to instruct this +library the list of extensions to apply to a model and the order to +apply them. This is not very convenient. Fortunately, an dedicated odoo +addon exists to make this process complete transparent. This addon is +called odoo-addon-extendable-fastapi.

    When you want to allow other addons to extend a pydantic model, you must -first define the model as an extendable model by using a dedicated metaclass

    +first define the model as an extendable model by using a dedicated +metaclass

     from pydantic import BaseModel
     from extendable_pydantic import ExtendableModelMeta
    @@ -1177,9 +1212,9 @@ 

    Extending the model used as para name = 0.1 model_config = ConfigDict(from_attributes=True)

    -

    As any other pydantic model, you can now use this model as parameter or as response -of a route handler. You can also use all the features of models defined with -pydantic.

    +

    As any other pydantic model, you can now use this model as parameter or +as response of a route handler. You can also use all the features of +models defined with pydantic.

     @demo_api_router.get(
         "/partner",
    @@ -1192,8 +1227,9 @@ 

    Extending the model used as para """Return the location""" return Partner.model_validate(partner)

    -

    If you need to add a new field into the model ‘Partner’, you can extend it -in your new addon by defining a new model that inherits from the model ‘Partner’.

    +

    If you need to add a new field into the model ‘Partner’, you can +extend it in your new addon by defining a new model that inherits from +the model ‘Partner’.

     from typing import Optional
     from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
    @@ -1201,60 +1237,63 @@ 

    Extending the model used as para class PartnerExtended(Partner, extends=Partner): email: Optional[str]

    -

    If your new addon is installed in a database, a call to the route handler -‘/demo/partner’ will return a response with the new field ‘email’ if a -value is provided by the odoo record.

    +

    If your new addon is installed in a database, a call to the route +handler ‘/demo/partner’ will return a response with the new field +‘email’ if a value is provided by the odoo record.

     {
       "name": "John Doe",
       "email": "jhon.doe@acsone.eu"
     }
     
    -

    If your new addon is not installed in a database, a call to the route handler -‘/demo/partner’ will only return the name of the partner.

    +

    If your new addon is not installed in a database, a call to the route +handler ‘/demo/partner’ will only return the name of the partner.

     {
       "name": "John Doe"
     }
     
    -
    -

    Note

    -

    The liskov substitution principle has also to be respected. That means that -if you extend a model, you must add new required fields or you must provide -default values for the new optional fields.

    +

    Note

    +

    The liskov substitution principle has also to be respected. That means +that if you extend a model, you must add new required fields or you must +provide default values for the new optional fields.

    +

    Managing security into the route handlers

    -

    By default the route handlers are processed using the user configured on the -‘fastapi.endpoint’ model instance. (default is the Public user). -You have seen previously how to define a dependency that will be used to enforce -the authentication of a partner. When a method depends on this dependency, the -‘authenticated_partner_id’ key is added to the context of the partner environment. -(If you don’t need the partner as dependency but need to get an environment -with the authenticated user, you can use the dependency ‘authenticated_partner_env’ instead of +

    By default the route handlers are processed using the user configured on +the ‘fastapi.endpoint’ model instance. (default is the Public user). +You have seen previously how to define a dependency that will be used to +enforce the authentication of a partner. When a method depends on this +dependency, the ‘authenticated_partner_id’ key is added to the context +of the partner environment. (If you don’t need the partner as dependency +but need to get an environment with the authenticated user, you can use +the dependency ‘authenticated_partner_env’ instead of ‘authenticated_partner’.)

    -

    The fastapi addon extends the ‘ir.rule’ model to add into the evaluation context -of the security rules the key ‘authenticated_partner_id’ that contains the id -of the authenticated partner.

    -

    As briefly introduced in a previous section, a good practice when you develop a -fastapi app and you want to protect your data in an efficient and traceable way is to:

    +

    The fastapi addon extends the ‘ir.rule’ model to add into the evaluation +context of the security rules the key ‘authenticated_partner_id’ that +contains the id of the authenticated partner.

    +

    As briefly introduced in a previous section, a good practice when you +develop a fastapi app and you want to protect your data in an efficient +and traceable way is to:

     <record
    @@ -1295,26 +1334,30 @@ 

    Managing security into the route

    How to test your fastapi app

    -

    Thanks to the starlette test client, it’s possible to test your fastapi app -in a very simple way. With the test client, you can call your route handlers -as if they were real http endpoints. The test client is available in the -‘fastapi.testclient’ module.

    -

    Once again the dependency injection mechanism comes to the rescue by allowing -you to inject into the test client specific implementations of the dependencies -normally provided by the normal processing of the request by the fastapi app. -(for example, you can inject a mock of the dependency ‘authenticated_partner’ -to test the behavior of your route handlers when the partner is not authenticated, -you can also inject a mock for the odoo_env etc…)

    -

    The fastapi addon provides a base class for the test cases that you can use to -write your tests. This base class is ‘odoo.fastapi.tests.common.FastAPITransactionCase’. -This class mainly provides the method ‘_create_test_client’ that you can -use to create a test client for your fastapi app. This method encapsulates the -creation of the test client and the injection of the dependencies. It also -ensures that the odoo environment is make available into the context of the -route handlers. This method is designed to be used when you need to test your -app or when you need to test a specific router (It’s therefore easy to defines -tests for routers in an addon that doesn’t provide a fastapi endpoint).

    -

    With this base class, writing a test for a route handler is as simple as:

    +

    Thanks to the starlette test client, it’s possible to test your fastapi +app in a very simple way. With the test client, you can call your route +handlers as if they were real http endpoints. The test client is +available in the ‘fastapi.testclient’ module.

    +

    Once again the dependency injection mechanism comes to the rescue by +allowing you to inject into the test client specific implementations of +the dependencies normally provided by the normal processing of the +request by the fastapi app. (for example, you can inject a mock of the +dependency ‘authenticated_partner’ to test the behavior of your route +handlers when the partner is not authenticated, you can also inject a +mock for the odoo_env etc…)

    +

    The fastapi addon provides a base class for the test cases that you can +use to write your tests. This base class is +‘odoo.fastapi.tests.common.FastAPITransactionCase’. This class +mainly provides the method ‘_create_test_client’ that you can use +to create a test client for your fastapi app. This method encapsulates +the creation of the test client and the injection of the dependencies. +It also ensures that the odoo environment is make available into the +context of the route handlers. This method is designed to be used when +you need to test your app or when you need to test a specific router +(It’s therefore easy to defines tests for routers in an addon that +doesn’t provide a fastapi endpoint).

    +

    With this base class, writing a test for a route handler is as simple +as:

     from odoo.fastapi.tests.common import FastAPITransactionCase
     
    @@ -1335,9 +1378,9 @@ 

    How to test your fastapi app self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(response.json(), {"Hello": "World"})

    -

    In the previous example, we created a test client for the demo_router. We could -have created a test client for the whole app by not specifying the router but -the app instead.

    +

    In the previous example, we created a test client for the demo_router. +We could have created a test client for the whole app by not specifying +the router but the app instead.

     from odoo.fastapi.tests.common import FastAPITransactionCase
     
    @@ -1362,129 +1405,142 @@ 

    How to test your fastapi app

    Overall considerations when you develop an fastapi app

    -

    Developing a fastapi app requires to follow some good practices to ensure that -the app is robust and easy to maintain. Here are some of them:

    +

    Developing a fastapi app requires to follow some good practices to +ensure that the app is robust and easy to maintain. Here are some of +them:

    -

    We could write a book about the best practices to follow when you design your api -but we will stop here. This list is the result of our experience at ACSONE SA/NV and it evolves over time. It’s a kind of rescue kit that we -would provide to a new developer that starts to design an api. This kit must -be accompanied with the reading of some useful resources link like the REST Guidelines. On a technical level, -the fastapi documentation provides a lot of -useful information as well, with a lot of examples. Last but not least, the -pydantic documentation is also very useful.

    +

    We could write a book about the best practices to follow when you design +your api but we will stop here. This list is the result of our +experience at ACSONE SA/NV and it evolves over +time. It’s a kind of rescue kit that we would provide to a new developer +that starts to design an api. This kit must be accompanied with the +reading of some useful resources link like the REST +Guidelines. On +a technical level, the fastapi +documentation provides a lot of +useful information as well, with a lot of examples. Last but not least, +the pydantic documentation is also very +useful.

    Miscellaneous

    -
    -
    -

    Development of a search route handler

    -

    The ‘odoo-addon-fastapi’ module provides 2 useful piece of code to help -you be consistent when writing a route handler for a search route.

    +

    Development of a search route handler

    +

    The ‘odoo-addon-fastapi’ module provides 2 useful piece of code to +help you be consistent when writing a route handler for a search route.

      -
    1. A dependency method to use to specify the pagination parameters in the same -way for all the search route handlers: ‘odoo.addons.fastapi.paging’.
    2. -
    3. A PagedCollection pydantic model to use to return the result of a search route -handler enclosed in a json document that contains the count of records.
    4. +
    5. A dependency method to use to specify the pagination parameters in +the same way for all the search route handlers: +‘odoo.addons.fastapi.paging’.
    6. +
    7. A PagedCollection pydantic model to use to return the result of a +search route handler enclosed in a json document that contains the +count of records.
     from typing import Annotated
    @@ -1517,33 +1573,36 @@ 

    Development of a search route ha items=[SaleOrder.model_validate(order) for order in orders], )

    -
    -

    Note

    -

    The ‘odoo.addons.fastapi.schemas.Paging’ and ‘odoo.addons.fastapi.schemas.PagedCollection’ -pydantic models are not designed to be extended to not introduce a -dependency between the ‘odoo-addon-fastapi’ module and the ‘odoo-addon-extendable’

    -
    +

    Note

    +

    The ‘odoo.addons.fastapi.schemas.Paging’ and +‘odoo.addons.fastapi.schemas.PagedCollection’ pydantic models are +not designed to be extended to not introduce a dependency between the +‘odoo-addon-fastapi’ module and the ‘odoo-addon-extendable’

    -

    Customization of the error handling

    -

    The error handling a very important topic in the design of the fastapi integration -with odoo. It must ensure that the error messages are properly return to the client -and that the transaction is properly roll backed. The ‘fastapi’ module provides -a way to register custom error handlers. The ‘odoo.addons.fastapi.error_handlers’ -module provides the default error handlers that are registered by default when -a new instance of the ‘FastAPI’ class is created. When an app is initialized in -‘fastapi.endpoint’ model, the method _get_app_exception_handlers is called to -get a dictionary of error handlers. This method is designed to be overridden -in a custom module to provide custom error handlers. You can override the handler -for a specific exception class or you can add a new handler for a new exception -or even replace all the handlers by your own handlers. Whatever you do, you must -ensure that the transaction is properly roll backed.

    -

    Some could argue that the error handling can’t be extended since the error handlers -are global method not defined in an odoo model. Since the method providing the -the error handlers definitions is defined on the ‘fastapi.endpoint’ model, it’s -not a problem at all, you just need to think another way to do it that by inheritance.

    -

    A solution could be to develop you own error handler to be able to process the -error and chain the call to the default error handler.

    +

    Customization of the error handling

    +

    The error handling a very important topic in the design of the fastapi +integration with odoo. It must ensure that the error messages are +properly return to the client and that the transaction is properly roll +backed. The ‘fastapi’ module provides a way to register custom error +handlers. The ‘odoo.addons.fastapi.error_handlers’ module provides +the default error handlers that are registered by default when a new +instance of the ‘FastAPI’ class is created. When an app is +initialized in ‘fastapi.endpoint’ model, the method +_get_app_exception_handlers is called to get a dictionary of error +handlers. This method is designed to be overridden in a custom module to +provide custom error handlers. You can override the handler for a +specific exception class or you can add a new handler for a new +exception or even replace all the handlers by your own handlers. +Whatever you do, you must ensure that the transaction is properly roll +backed.

    +

    Some could argue that the error handling can’t be extended since the +error handlers are global method not defined in an odoo model. Since the +method providing the the error handlers definitions is defined on the +‘fastapi.endpoint’ model, it’s not a problem at all, you just need to +think another way to do it that by inheritance.

    +

    A solution could be to develop you own error handler to be able to +process the error and chain the call to the default error handler.

     class MyCustomErrorHandler():
         def __init__(self, next_handler):
    @@ -1555,8 +1614,9 @@ 

    Customization of the error handl # do something with the response return response

    -

    With this solution, you can now register your custom error handler by overriding -the method _get_app_exception_handlers in your custom module.

    +

    With this solution, you can now register your custom error handler by +overriding the method _get_app_exception_handlers in your custom +module.

     class FastapiEndpoint(models.Model):
         _inherit = "fastapi.endpoint"
    @@ -1572,20 +1632,22 @@ 

    Customization of the error handl handlers[odoo.exceptions.AccessError] = MyCustomErrorHandler(access_error_handler) return handlers

    -

    In the previous example, we extend the error handler for the ‘AccessError’ exception -for all the endpoints. You can do the same for a specific app by checking the -‘app’ field of the ‘fastapi.endpoint’ record before registering your custom error -handler.

    +

    In the previous example, we extend the error handler for the +‘AccessError’ exception for all the endpoints. You can do the same for a +specific app by checking the ‘app’ field of the ‘fastapi.endpoint’ +record before registering your custom error handler.

    -

    FastAPI addons directory structure

    -

    When you develop a new addon to expose an api with fastapi, it’s a good practice -to follow the same directory structure and naming convention for the files -related to the api. It will help you to easily find the files related to the api -and it will help the other developers to understand your code.

    -

    Here is the directory structure that we recommend. It’s based on practices that -are used in the python community when developing a fastapi app.

    -
    +

    FastAPI addons directory structure

    +

    When you develop a new addon to expose an api with fastapi, it’s a good +practice to follow the same directory structure and naming convention +for the files related to the api. It will help you to easily find the +files related to the api and it will help the other developers to +understand your code.

    +

    Here is the directory structure that we recommend. It’s based on +practices that are used in the python community when developing a +fastapi app.

    +
     .
     ├── x_api
     │   ├── data
    @@ -1616,17 +1678,18 @@ 

    FastAPI addons directory structu │   ├── error_handlers.py # custom error handlers

    +
    +

    What’s next?

    -

    The ‘odoo-addon-fastapi’ module is still in its early stage of development. -It will evolve over time to integrate your feedback and to provide the missing -features. It’s now up to you to try it and to provide your feedback.

    +

    The ‘odoo-addon-fastapi’ module is still in its early stage of +development. It will evolve over time to integrate your feedback and to +provide the missing features. It’s now up to you to try it and to +provide your feedback.

    Known issues / Roadmap

    -

    The roadmap -and known issues can -be found on GitHub.

    -

    The FastAPI module provides an easy way to use WebSockets. Unfortunately, this -support is not ‘yet’ available in the Odoo framework. The challenge is high -because the integration of the fastapi is based on the use of a specific middleware -that convert the WSGI request consumed by odoo to a ASGI request. The question -is to know if it is also possible to develop the same kind of bridge for the +

    The +roadmap +and known +issues +can be found on GitHub.

    +

    The FastAPI module provides an easy way to use WebSockets. +Unfortunately, this support is not ‘yet’ available in the Odoo +framework. The challenge is high because the integration of the fastapi +is based on the use of a specific middleware that convert the WSGI +request consumed by odoo to a ASGI request. The question is to know if +it is also possible to develop the same kind of bridge for the WebSockets and to stream large responses.

    @@ -1717,9 +1787,11 @@

    16.0.1.2.5 (2024-01-17)

    16.0.1.2.3 (2023-12-21)

    Bugfixes

    @@ -1727,23 +1799,28 @@

    16.0.1.2.3 (2023-12-21)

    16.0.1.2.2 (2023-12-12)

    Bugfixes

    16.0.1.2.1 (2023-11-03)

    Bugfixes

    @@ -1751,13 +1828,15 @@

    16.0.1.2.1 (2023-11-03)

    16.0.1.2.0 (2023-10-13)

    Features

    @@ -1766,7 +1845,7 @@

    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.

    +feedback.

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

    @@ -1792,7 +1871,7 @@

    Maintainers

    promote its widespread use.

    Current maintainer:

    lmignon

    -

    This module is part of the OCA/rest-framework project on GitHub.

    +

    This module is part of the OCA/rest-framework project on GitHub.

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

    diff --git a/fastapi/tests/common.py b/fastapi/tests/common.py index 11bcc1b5..2ed646dc 100644 --- a/fastapi/tests/common.py +++ b/fastapi/tests/common.py @@ -1,8 +1,9 @@ # Copyright 2023 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). +from collections.abc import Callable from contextlib import contextmanager from functools import partial -from typing import Any, Callable, Dict +from typing import Any from odoo.api import Environment from odoo.tests import tagged @@ -54,7 +55,7 @@ def setUpClass(cls): cls.default_fastapi_odoo_env: Environment = cls.env cls.default_fastapi_running_user: Users | None = None cls.default_fastapi_authenticated_partner: Partner | None = None - cls.default_fastapi_dependency_overrides: Dict[ + cls.default_fastapi_dependency_overrides: dict[ Callable[..., Any], Callable[..., Any] ] = {} @@ -66,7 +67,7 @@ def _create_test_client( user: Users | None = None, partner: Partner | None = None, env: Environment = None, - dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = None, + dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] = None, raise_server_exceptions: bool = True, ): """ diff --git a/fastapi/views/fastapi_endpoint.xml b/fastapi/views/fastapi_endpoint.xml index e4a6c28c..5b0d0a46 100644 --- a/fastapi/views/fastapi_endpoint.xml +++ b/fastapi/views/fastapi_endpoint.xml @@ -20,20 +20,20 @@ type="object" class="oe_highlight" icon="fa-refresh" - attrs="{'invisible': [('registry_sync', '=', True)]}" + invisible="registry_sync" />