From e7d1440a003b445925a39566fab05a63fe6d598e Mon Sep 17 00:00:00 2001 From: Yair Siman Tov <63305203+yairsimantov20@users.noreply.github.com> Date: Sun, 18 Feb 2024 13:07:09 +0200 Subject: [PATCH] Send logs to Port integration (#371) --- CHANGELOG.md | 19 ++ integrations/jira/jira/client.py | 1 + poetry.lock | 311 +++++++++--------- port_ocean/bootstrap.py | 2 +- port_ocean/cli/commands/defaults/clean.py | 12 +- port_ocean/cli/commands/defaults/dock.py | 12 +- port_ocean/clients/port/authentication.py | 3 +- .../clients/port/mixins/integrations.py | 27 +- port_ocean/config/base.py | 29 ++ port_ocean/config/dynamic.py | 8 +- port_ocean/config/settings.py | 19 +- port_ocean/consumers/base_consumer.py | 7 - port_ocean/consumers/kafka_consumer.py | 16 +- port_ocean/context/event.py | 2 +- port_ocean/context/ocean.py | 55 ++-- port_ocean/context/utils.py | 24 -- port_ocean/core/event_listener/base.py | 26 +- port_ocean/core/event_listener/http.py | 5 +- port_ocean/core/event_listener/kafka.py | 17 +- port_ocean/core/event_listener/once.py | 4 +- port_ocean/core/event_listener/polling.py | 12 +- port_ocean/exceptions/context.py | 4 + port_ocean/exceptions/utils.py | 9 + port_ocean/log/__init__.py | 0 port_ocean/log/handlers.py | 88 +++++ port_ocean/log/logger_setup.py | 67 ++++ port_ocean/log/sensetive.py | 51 +++ port_ocean/logger_setup.py | 38 --- port_ocean/middlewares.py | 2 +- port_ocean/ocean.py | 24 +- port_ocean/run.py | 14 +- port_ocean/utils/__init__.py | 3 + port_ocean/utils/async_http.py | 29 ++ port_ocean/utils/misc.py | 56 ++++ port_ocean/{utils.py => utils/repeat.py} | 86 +---- port_ocean/utils/signal.py | 54 +++ port_ocean/version.py | 2 +- pyproject.toml | 2 +- 38 files changed, 735 insertions(+), 405 deletions(-) delete mode 100644 port_ocean/consumers/base_consumer.py delete mode 100644 port_ocean/context/utils.py create mode 100644 port_ocean/exceptions/utils.py create mode 100644 port_ocean/log/__init__.py create mode 100644 port_ocean/log/handlers.py create mode 100644 port_ocean/log/logger_setup.py create mode 100644 port_ocean/log/sensetive.py delete mode 100644 port_ocean/logger_setup.py create mode 100644 port_ocean/utils/__init__.py create mode 100644 port_ocean/utils/async_http.py create mode 100644 port_ocean/utils/misc.py rename port_ocean/{utils.py => utils/repeat.py} (51%) create mode 100644 port_ocean/utils/signal.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 509052242d..3381c0f486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm +## 0.5.0 (2024-02-18) + + +### Features + +- Added a method for ocean integration to redact sensitive information from the logs and automatically apply it to sensitive configurations and known sensitive patterns. (#1) +- Added an HTTP handler for Ocean logs to facilitate sending the logs to the Port. (#2) + +### Improvements + +- Seperated the `port_ocean.utils` file into multiple files within the `utils` folder to improve code organization. (#1) +- Changed the Ocean context to be a global variable instead of using Localstack, preventing the framework from re-initiating the context for each thread. (#2) + +### Bug Fixes + +- Fixed an issue where the event listener was causing the application to continue running even after receiving a termination signal. (#1) +- Fixed a bug that caused some termination signal handlers to not work by consolidating the signal listeners in a single class, as signals can only have one listener. (#2) + + ## 0.4.17 (2024-01-23) diff --git a/integrations/jira/jira/client.py b/integrations/jira/jira/client.py index 5650fefb2f..b10f14af90 100644 --- a/integrations/jira/jira/client.py +++ b/integrations/jira/jira/client.py @@ -4,6 +4,7 @@ from httpx import Timeout, BasicAuth from jira.overrides import JiraResourceConfig from loguru import logger + from port_ocean.context.event import event from port_ocean.context.ocean import ocean from port_ocean.utils import http_async_client diff --git a/poetry.lock b/poetry.lock index 3884565ae3..fb5ee2bdbd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "anyio" @@ -70,33 +70,33 @@ chardet = ">=3.0.2" [[package]] name = "black" -version = "23.12.0" +version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67f19562d367468ab59bd6c36a72b2c84bc2f16b59788690e02bbcb140a77175"}, - {file = "black-23.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbd75d9f28a7283b7426160ca21c5bd640ca7cd8ef6630b4754b6df9e2da8462"}, - {file = "black-23.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:593596f699ca2dcbbbdfa59fcda7d8ad6604370c10228223cd6cf6ce1ce7ed7e"}, - {file = "black-23.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:12d5f10cce8dc27202e9a252acd1c9a426c83f95496c959406c96b785a92bb7d"}, - {file = "black-23.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e73c5e3d37e5a3513d16b33305713237a234396ae56769b839d7c40759b8a41c"}, - {file = "black-23.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba09cae1657c4f8a8c9ff6cfd4a6baaf915bb4ef7d03acffe6a2f6585fa1bd01"}, - {file = "black-23.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace64c1a349c162d6da3cef91e3b0e78c4fc596ffde9413efa0525456148873d"}, - {file = "black-23.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:72db37a2266b16d256b3ea88b9affcdd5c41a74db551ec3dd4609a59c17d25bf"}, - {file = "black-23.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fdf6f23c83078a6c8da2442f4d4eeb19c28ac2a6416da7671b72f0295c4a697b"}, - {file = "black-23.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39dda060b9b395a6b7bf9c5db28ac87b3c3f48d4fdff470fa8a94ab8271da47e"}, - {file = "black-23.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7231670266ca5191a76cb838185d9be59cfa4f5dd401b7c1c70b993c58f6b1b5"}, - {file = "black-23.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:193946e634e80bfb3aec41830f5d7431f8dd5b20d11d89be14b84a97c6b8bc75"}, - {file = "black-23.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcf91b01ddd91a2fed9a8006d7baa94ccefe7e518556470cf40213bd3d44bbbc"}, - {file = "black-23.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:996650a89fe5892714ea4ea87bc45e41a59a1e01675c42c433a35b490e5aa3f0"}, - {file = "black-23.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdbff34c487239a63d86db0c9385b27cdd68b1bfa4e706aa74bb94a435403672"}, - {file = "black-23.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:97af22278043a6a1272daca10a6f4d36c04dfa77e61cbaaf4482e08f3640e9f0"}, - {file = "black-23.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ead25c273adfad1095a8ad32afdb8304933efba56e3c1d31b0fee4143a1e424a"}, - {file = "black-23.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c71048345bdbced456cddf1622832276d98a710196b842407840ae8055ade6ee"}, - {file = "black-23.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a832b6e00eef2c13b3239d514ea3b7d5cc3eaa03d0474eedcbbda59441ba5d"}, - {file = "black-23.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:6a82a711d13e61840fb11a6dfecc7287f2424f1ca34765e70c909a35ffa7fb95"}, - {file = "black-23.12.0-py3-none-any.whl", hash = "sha256:a7c07db8200b5315dc07e331dda4d889a56f6bf4db6a9c2a526fa3166a81614f"}, - {file = "black-23.12.0.tar.gz", hash = "sha256:330a327b422aca0634ecd115985c1c7fd7bdb5b5a2ef8aa9888a82e2ebe9437a"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] @@ -114,13 +114,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -332,17 +332,18 @@ rich = "*" [[package]] name = "dill" -version = "0.3.7" +version = "0.3.8" description = "serialize all of Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, - {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "fastapi" @@ -591,71 +592,71 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -761,28 +762,28 @@ files = [ [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -791,47 +792,47 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "1.10.13" +version = "1.10.14" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, - {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, - {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, - {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, - {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, - {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, + {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, + {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, + {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, + {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, + {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, + {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, + {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, + {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, ] [package.dependencies] @@ -918,13 +919,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -952,13 +953,13 @@ six = ">=1.5" [[package]] name = "python-slugify" -version = "8.0.1" +version = "8.0.4" description = "A Python slugify application that also handles Unicode" optional = true python-versions = ">=3.7" files = [ - {file = "python-slugify-8.0.1.tar.gz", hash = "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"}, - {file = "python_slugify-8.0.1-py2.py3-none-any.whl", hash = "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395"}, + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, ] [package.dependencies] @@ -979,7 +980,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -987,15 +987,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1012,7 +1005,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1020,7 +1012,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1184,13 +1175,13 @@ dev = ["furo", "packaging", "sphinx (>=5)", "twisted"] [[package]] name = "types-python-dateutil" -version = "2.8.19.14" +version = "2.8.19.20240106" description = "Typing stubs for python-dateutil" optional = true -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, + {file = "types-python-dateutil-2.8.19.20240106.tar.gz", hash = "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f"}, + {file = "types_python_dateutil-2.8.19.20240106-py3-none-any.whl", hash = "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"}, ] [[package]] @@ -1372,13 +1363,13 @@ files = [ [[package]] name = "yamllint" -version = "1.33.0" +version = "1.35.0" description = "A linter for YAML files." optional = false python-versions = ">=3.8" files = [ - {file = "yamllint-1.33.0-py3-none-any.whl", hash = "sha256:28a19f5d68d28d8fec538a1db21bb2d84c7dc2e2ea36266da8d4d1c5a683814d"}, - {file = "yamllint-1.33.0.tar.gz", hash = "sha256:2dceab9ef2d99518a2fcf4ffc964d44250ac4459be1ba3ca315118e4a1a81f7d"}, + {file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"}, + {file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"}, ] [package.dependencies] diff --git a/port_ocean/bootstrap.py b/port_ocean/bootstrap.py index 117a35efcb..939dd3aa32 100644 --- a/port_ocean/bootstrap.py +++ b/port_ocean/bootstrap.py @@ -7,7 +7,7 @@ from port_ocean.core.integrations.base import BaseIntegration from port_ocean.ocean import Ocean -from port_ocean.utils import load_module +from port_ocean.utils.misc import load_module def _get_base_integration_class_from_module( diff --git a/port_ocean/cli/commands/defaults/clean.py b/port_ocean/cli/commands/defaults/clean.py index d1d8cacb53..888e2a2ffd 100644 --- a/port_ocean/cli/commands/defaults/clean.py +++ b/port_ocean/cli/commands/defaults/clean.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -import click - from inspect import getmembers -from port_ocean.utils import load_module -from .group import defaults +import click + +from port_ocean.bootstrap import create_default_app from port_ocean.cli.commands.main import print_logo, console -from port_ocean.ocean import Ocean from port_ocean.core.defaults import clean_defaults -from port_ocean.bootstrap import create_default_app +from port_ocean.ocean import Ocean +from port_ocean.utils.misc import load_module +from .group import defaults @defaults.command() diff --git a/port_ocean/cli/commands/defaults/dock.py b/port_ocean/cli/commands/defaults/dock.py index 32c881bc3a..71675aefef 100644 --- a/port_ocean/cli/commands/defaults/dock.py +++ b/port_ocean/cli/commands/defaults/dock.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -import click - from inspect import getmembers -from port_ocean.utils import load_module -from .group import defaults +import click + +from port_ocean.bootstrap import create_default_app from port_ocean.cli.commands.main import print_logo, console -from port_ocean.ocean import Ocean from port_ocean.core.defaults.initialize import initialize_defaults -from port_ocean.bootstrap import create_default_app +from port_ocean.ocean import Ocean +from port_ocean.utils.misc import load_module +from .group import defaults @defaults.command() diff --git a/port_ocean/clients/port/authentication.py b/port_ocean/clients/port/authentication.py index d1f8ef47ee..1ddd506aeb 100644 --- a/port_ocean/clients/port/authentication.py +++ b/port_ocean/clients/port/authentication.py @@ -6,7 +6,7 @@ from port_ocean.clients.port.types import UserAgentType from port_ocean.clients.port.utils import handle_status_code -from port_ocean.utils import get_time +from port_ocean.utils.misc import get_time class TokenResponse(BaseModel): @@ -79,5 +79,4 @@ async def token(self) -> str: self.last_token_object = await self._get_token( self.client_id, self.client_secret ) - return self.last_token_object.full_token diff --git a/port_ocean/clients/port/mixins/integrations.py b/port_ocean/clients/port/mixins/integrations.py index ac399019a0..3f357d0975 100644 --- a/port_ocean/clients/port/mixins/integrations.py +++ b/port_ocean/clients/port/mixins/integrations.py @@ -1,4 +1,4 @@ -from typing import Any, TYPE_CHECKING, Optional +from typing import Any, TYPE_CHECKING, Optional, TypedDict import httpx from loguru import logger @@ -11,6 +11,10 @@ from port_ocean.core.handlers.port_app_config.models import PortAppConfig +class LogAttributes(TypedDict): + ingestUrl: str + + class IntegrationClientMixin: def __init__( self, @@ -23,6 +27,7 @@ def __init__( self.integration_version = integration_version self.auth = auth self.client = client + self._log_attributes: LogAttributes | None = None async def _get_current_integration(self) -> httpx.Response: logger.info(f"Fetching integration with id: {self.integration_identifier}") @@ -39,6 +44,12 @@ async def get_current_integration( handle_status_code(response, should_raise, should_log) return response.json()["integration"] + async def get_log_attributes(self) -> LogAttributes: + if self._log_attributes is None: + response = await self.get_current_integration() + self._log_attributes = response["logAttributes"] + return self._log_attributes + async def create_integration( self, _type: str, @@ -112,3 +123,17 @@ async def initialize_integration( logger.info( f"Integration with id: {self.integration_identifier} successfully registered" ) + + async def ingest_integration_logs(self, logs: list[dict[str, Any]]) -> None: + logger.debug("Ingesting logs") + log_attributes = await self.get_log_attributes() + headers = await self.auth.headers() + response = await self.client.post( + log_attributes["ingestUrl"], + headers=headers, + json={ + "logs": logs, + }, + ) + handle_status_code(response) + logger.debug("Logs successfully ingested") diff --git a/port_ocean/config/base.py b/port_ocean/config/base.py index ace0db1a66..de6b089951 100644 --- a/port_ocean/config/base.py +++ b/port_ocean/config/base.py @@ -131,6 +131,9 @@ def load_providers( class BaseOceanSettings(BaseSettings): base_path: str + def get_sensitive_fields_data(self) -> set[str]: + return _get_sensitive_information(self) + class Config: yaml_file = "./config.yaml" env_prefix = "OCEAN__" @@ -153,3 +156,29 @@ def customise_sources( # type: ignore s, env_settings(s), init_settings.init_kwargs["base_path"] ), ) + + +class BaseOceanModel(BaseModel): + def get_sensitive_fields_data(self) -> set[str]: + return _get_sensitive_information(self) + + +def _get_sensitive_information( + model: BaseOceanModel | BaseSettings, +) -> set[str]: + sensitive_fields = [ + field_name + for field_name, field in model.__fields__.items() + if field.field_info.extra.get("sensitive", False) + ] + sensitive_set = {str(getattr(model, field_name)) for field_name in sensitive_fields} + + recursive_sensitive_data = [ + getattr(model, field_name).get_sensitive_fields_data() + for field_name, field in model.__fields__.items() + if isinstance(getattr(model, field_name), BaseOceanModel) + ] + for sensitive_data in recursive_sensitive_data: + sensitive_set.update(sensitive_data) + + return sensitive_set diff --git a/port_ocean/config/dynamic.py b/port_ocean/config/dynamic.py index 495f55dc2a..b8a10229c6 100644 --- a/port_ocean/config/dynamic.py +++ b/port_ocean/config/dynamic.py @@ -3,7 +3,9 @@ from humps import decamelize from pydantic import BaseModel, AnyUrl, create_model, Extra, parse_obj_as, validator -from pydantic.fields import ModelField +from pydantic.fields import ModelField, Field + +from port_ocean.config.base import BaseOceanModel class Configuration(BaseModel, extra=Extra.allow): @@ -11,6 +13,7 @@ class Configuration(BaseModel, extra=Extra.allow): type: str required: bool = False default: Optional[Any] + sensitive: bool = False def dynamic_parse(value: Any, field: ModelField) -> Any: @@ -51,11 +54,12 @@ def default_config_factory(configurations: Any) -> Type[BaseModel]: default = parse_obj_as(field_type, config.default) fields[decamelize(config.name)] = ( field_type, - default, + Field(default, sensitive=config.sensitive), ) dynamic_model = create_model( # type: ignore __model_name="Config", + __base__=BaseOceanModel, **fields, __validators__={"dynamic_parse": validator("*", pre=True)(dynamic_parse)}, ) diff --git a/port_ocean/config/settings.py b/port_ocean/config/settings.py index 0472511358..7e3f0e0e18 100644 --- a/port_ocean/config/settings.py +++ b/port_ocean/config/settings.py @@ -1,15 +1,18 @@ from typing import Any, Literal -from pydantic import BaseSettings, BaseModel, Extra, AnyHttpUrl, parse_obj_as, validator +from pydantic import Extra, AnyHttpUrl, parse_obj_as, validator +from pydantic.fields import Field +from pydantic.main import BaseModel -from port_ocean.config.base import BaseOceanSettings +from port_ocean.config.base import BaseOceanSettings, BaseOceanModel from port_ocean.core.event_listener import EventListenerSettingsType LogLevelType = Literal["ERROR", "WARNING", "INFO", "DEBUG", "CRITICAL"] -class ApplicationSettings(BaseSettings): +class ApplicationSettings(BaseOceanModel): log_level: LogLevelType = "INFO" + enable_http_logging: bool = True port: int = 8000 class Config: @@ -22,16 +25,16 @@ def customise_sources(cls, init_settings, env_settings, *_, **__): # type: igno return env_settings, init_settings -class PortSettings(BaseModel, extra=Extra.allow): - client_id: str - client_secret: str +class PortSettings(BaseOceanModel, extra=Extra.allow): + client_id: str = Field(..., sensitive=True) + client_secret: str = Field(..., sensitive=True) base_url: AnyHttpUrl = parse_obj_as(AnyHttpUrl, "https://api.getport.io") -class IntegrationSettings(BaseModel, extra=Extra.allow): +class IntegrationSettings(BaseOceanModel, extra=Extra.allow): identifier: str type: str - config: dict[str, Any] + config: dict[str, Any] | BaseModel @validator("identifier", "type") def validate_lower(cls, v: str) -> str: diff --git a/port_ocean/consumers/base_consumer.py b/port_ocean/consumers/base_consumer.py deleted file mode 100644 index 56d87096d7..0000000000 --- a/port_ocean/consumers/base_consumer.py +++ /dev/null @@ -1,7 +0,0 @@ -from abc import ABC, abstractmethod - - -class BaseConsumer(ABC): - @abstractmethod - def start(self) -> None: - pass diff --git a/port_ocean/consumers/kafka_consumer.py b/port_ocean/consumers/kafka_consumer.py index d3862f3608..1599e41157 100644 --- a/port_ocean/consumers/kafka_consumer.py +++ b/port_ocean/consumers/kafka_consumer.py @@ -1,12 +1,10 @@ -import signal +import threading from typing import Any, Callable from confluent_kafka import Consumer, KafkaException, Message # type: ignore from loguru import logger from pydantic import BaseModel -from port_ocean.consumers.base_consumer import BaseConsumer - class KafkaConsumerConfig(BaseModel): brokers: str @@ -19,7 +17,7 @@ class KafkaConsumerConfig(BaseModel): consumer_poll_timeout: int -class KafkaConsumer(BaseConsumer): +class KafkaConsumer: def __init__( self, msg_process: Callable[[Message], None], @@ -30,9 +28,6 @@ def __init__( self.org_id = org_id self.config = config - signal.signal(signal.SIGINT, self.exit_gracefully) - signal.signal(signal.SIGTERM, self.exit_gracefully) - self.msg_process = msg_process if config.kafka_security_enabled: kafka_config = { @@ -53,7 +48,8 @@ def __init__( self.consumer = Consumer(kafka_config) - def start(self) -> None: + def start(self, event: threading.Event) -> None: + self.running = True try: logger.info("Start consumer...") @@ -64,8 +60,7 @@ def start(self) -> None: ), ) logger.info("Subscribed to topics") - self.running = True - while self.running: + while self.running and not event.is_set(): try: msg = self.consumer.poll(timeout=self.config.consumer_poll_timeout) if msg is None: @@ -90,6 +85,7 @@ def start(self) -> None: except Exception as message_error: logger.error(str(message_error)) finally: + logger.info("Closing consumer...") self.exit_gracefully() def exit_gracefully(self, *_: Any) -> None: diff --git a/port_ocean/context/event.py b/port_ocean/context/event.py index b95ae10d09..0e83408a4a 100644 --- a/port_ocean/context/event.py +++ b/port_ocean/context/event.py @@ -22,7 +22,7 @@ EventContextNotFoundError, ResourceContextNotFoundError, ) -from port_ocean.utils import get_time +from port_ocean.utils.misc import get_time if TYPE_CHECKING: from port_ocean.core.handlers.port_app_config.models import ( diff --git a/port_ocean/context/ocean.py b/port_ocean/context/ocean.py index fd08497854..0830729788 100644 --- a/port_ocean/context/ocean.py +++ b/port_ocean/context/ocean.py @@ -1,8 +1,8 @@ -from dataclasses import dataclass -from typing import Callable, TYPE_CHECKING, Any, Literal +from typing import Callable, TYPE_CHECKING, Any, Literal, Union from fastapi import APIRouter -from werkzeug.local import LocalProxy, LocalStack +from pydantic.main import BaseModel +from werkzeug.local import LocalProxy from port_ocean.clients.port.types import UserAgentType from port_ocean.core.models import Entity @@ -12,7 +12,10 @@ RawEntityDiff, EntityDiff, ) -from port_ocean.exceptions.context import PortOceanContextNotFoundError +from port_ocean.exceptions.context import ( + PortOceanContextNotFoundError, + PortOceanContextAlreadyInitializedError, +) if TYPE_CHECKING: from port_ocean.config.settings import IntegrationConfiguration @@ -21,9 +24,21 @@ from port_ocean.clients.port.client import PortClient -@dataclass class PortOceanContext: - app: "Ocean" + def __init__(self, app: Union["Ocean", None]) -> None: + self._app = app + + @property + def app(self) -> "Ocean": + if self._app is None: + raise PortOceanContextNotFoundError( + "You must first initialize PortOcean in order to use it" + ) + return self._app + + @property + def initialized(self) -> bool: + return self._app is not None @property def config(self) -> "IntegrationConfiguration": @@ -39,6 +54,8 @@ def integration(self) -> "BaseIntegration": @property def integration_config(self) -> dict[str, Any]: + if isinstance(self.app.config.integration.config, BaseModel): + return self.app.config.integration.config.dict() return self.app.config.integration.config @property @@ -118,27 +135,17 @@ async def sync_raw_all(self) -> None: await self.integration.sync_raw_all(trigger_type="manual") -_port_ocean_context_stack: LocalStack[PortOceanContext] = LocalStack() +_port_ocean: PortOceanContext = PortOceanContext(None) def initialize_port_ocean_context(ocean_app: "Ocean") -> None: - """ - This Function initializes the PortOcean context and pushes it into the LocalStack(). - """ - _port_ocean_context_stack.push(PortOceanContext(app=ocean_app)) - - -def _get_port_ocean_context() -> PortOceanContext: - """ - Get the PortOcean context from the current thread. - """ - port_ocean_context = _port_ocean_context_stack.top - if port_ocean_context is None: - raise PortOceanContextNotFoundError( - "You must first initialize PortOcean in order to use it" - ) + global _port_ocean - return port_ocean_context + if _port_ocean.initialized: + raise PortOceanContextAlreadyInitializedError( + "PortOcean context is already initialized" + ) + _port_ocean = PortOceanContext(app=ocean_app) -ocean: PortOceanContext = LocalProxy(lambda: _get_port_ocean_context()) # type: ignore +ocean: PortOceanContext = LocalProxy(lambda: _port_ocean) # type: ignore diff --git a/port_ocean/context/utils.py b/port_ocean/context/utils.py deleted file mode 100644 index 21f9cfd122..0000000000 --- a/port_ocean/context/utils.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Callable - -from port_ocean.context.ocean import ( - initialize_port_ocean_context, - ocean, -) - - -def wrap_method_with_context( - func: Callable[..., None], -) -> Callable[..., None]: - """ - A method that wraps a method and initializing the PortOceanContext and invoking the given function. - - :param func: The function to be wrapped. - """ - # assign the current ocean app to a variable - ocean_app = ocean.app - - def wrapper(*args, **kwargs) -> None: # type: ignore - initialize_port_ocean_context(ocean_app=ocean_app) - func(*args, **kwargs) - - return wrapper diff --git a/port_ocean/core/event_listener/base.py b/port_ocean/core/event_listener/base.py index aa6394be0c..898bde0291 100644 --- a/port_ocean/core/event_listener/base.py +++ b/port_ocean/core/event_listener/base.py @@ -1,7 +1,11 @@ from abc import abstractmethod +from asyncio import Task from typing import TypedDict, Callable, Any, Awaitable -from pydantic import BaseModel, Extra +from pydantic import Extra + +from port_ocean.config.base import BaseOceanModel +from port_ocean.utils.signal import signal_handler class EventListenerEvents(TypedDict): @@ -18,13 +22,29 @@ def __init__( events: EventListenerEvents, ): self.events = events + self._tasks_to_close: list[Task[Any]] = [] - @abstractmethod async def start(self) -> None: + signal_handler.register(self._stop) + await self._start() + + @abstractmethod + async def _start(self) -> None: + pass + + def stop(self) -> None: + self._stop() + for task in self._tasks_to_close: + task.cancel() + + def _stop(self) -> None: + """ + Can be used for event listeners that need cleanup before exiting. + """ pass -class EventListenerSettings(BaseModel, extra=Extra.allow): +class EventListenerSettings(BaseOceanModel, extra=Extra.allow): type: str def to_request(self) -> dict[str, Any]: diff --git a/port_ocean/core/event_listener/http.py b/port_ocean/core/event_listener/http.py index ca35457af2..69083daa00 100644 --- a/port_ocean/core/event_listener/http.py +++ b/port_ocean/core/event_listener/http.py @@ -3,6 +3,7 @@ from fastapi import APIRouter from loguru import logger from pydantic import AnyHttpUrl +from pydantic.fields import Field from port_ocean.context.ocean import ocean from port_ocean.core.event_listener.base import ( @@ -24,7 +25,7 @@ class HttpEventListenerSettings(EventListenerSettings): """ type: Literal["WEBHOOK"] - app_host: AnyHttpUrl + app_host: AnyHttpUrl = Field(..., sensitive=True) def to_request(self) -> dict[str, Any]: return { @@ -53,7 +54,7 @@ def __init__( super().__init__(events) self.event_listener_config = event_listener_config - async def start(self) -> None: + async def _start(self) -> None: """ Starts the HTTP event listener. It sets up an APIRouter to handle the `/resync` endpoint and registers the "on_resync" event handler. diff --git a/port_ocean/core/event_listener/kafka.py b/port_ocean/core/event_listener/kafka.py index df3ad88f08..4ab52cf87b 100644 --- a/port_ocean/core/event_listener/kafka.py +++ b/port_ocean/core/event_listener/kafka.py @@ -11,7 +11,6 @@ from port_ocean.context.ocean import ( ocean, ) -from port_ocean.context.utils import wrap_method_with_context from port_ocean.core.event_listener.base import ( BaseEventListener, EventListenerEvents, @@ -71,6 +70,8 @@ def __init__( self.org_id = org_id self.integration_identifier = integration_identifier self.integration_type = integration_type + self._consumer_kill_event = threading.Event() + self._running_task = None async def _get_kafka_config(self) -> KafkaConsumerConfig: """ @@ -120,7 +121,9 @@ async def try_wrapper() -> None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - loop.run_until_complete(try_wrapper()) + running_task = loop.create_task(try_wrapper()) + self._tasks_to_close.append(running_task) + loop.run_until_complete(running_task) def _handle_message(self, raw_msg: Message) -> None: """ @@ -141,12 +144,12 @@ def _handle_message(self, raw_msg: Message) -> None: logger.info(f"spawning thread {thread_name} to start resync") threading.Thread( name=thread_name, - target=wrap_method_with_context(self._resync_in_new_event_loop), + target=self._resync_in_new_event_loop, args=(message,), ).start() logger.info(f"thread {thread_name} started") - async def start(self) -> None: + async def _start(self) -> None: """ The main method that starts the Kafka consumer. It creates a KafkaConsumer instance with the given configuration and starts it in a separate thread. @@ -159,5 +162,9 @@ async def start(self) -> None: logger.info("Starting Kafka consumer") threading.Thread( name="ocean_kafka_consumer", - target=wrap_method_with_context(func=consumer.start), + target=consumer.start, + args=(self._consumer_kill_event,), ).start() + + def _stop(self) -> None: + self._consumer_kill_event.set() diff --git a/port_ocean/core/event_listener/once.py b/port_ocean/core/event_listener/once.py index 9d437c7585..15154a4c35 100644 --- a/port_ocean/core/event_listener/once.py +++ b/port_ocean/core/event_listener/once.py @@ -8,7 +8,7 @@ EventListenerEvents, EventListenerSettings, ) -from port_ocean.utils import repeat_every +from port_ocean.utils.repeat import repeat_every class OnceEventListenerSettings(EventListenerSettings): @@ -42,7 +42,7 @@ def __init__( super().__init__(events) self.event_listener_config = event_listener_config - async def start(self) -> None: + async def _start(self) -> None: """ Starts the resync process, and exits the application once finished. """ diff --git a/port_ocean/core/event_listener/polling.py b/port_ocean/core/event_listener/polling.py index f747c24615..d69a540806 100644 --- a/port_ocean/core/event_listener/polling.py +++ b/port_ocean/core/event_listener/polling.py @@ -1,3 +1,4 @@ +from asyncio import Task, get_event_loop from typing import Literal, Any from loguru import logger @@ -8,7 +9,7 @@ EventListenerEvents, EventListenerSettings, ) -from port_ocean.utils import repeat_every +from port_ocean.utils.repeat import repeat_every class PollingEventListenerSettings(EventListenerSettings): @@ -49,7 +50,7 @@ def __init__( self.event_listener_config = event_listener_config self._last_updated_at = None - async def start(self) -> None: + async def _start(self) -> None: """ Starts the polling event listener. It registers the "on_resync" event to be called every `interval` seconds specified in the `event_listener_config`. @@ -75,7 +76,12 @@ async def resync() -> None: if should_resync: logger.info("Detected change in integration, resyncing") self._last_updated_at = last_updated_at - await self.events["on_resync"]({}) + running_task: Task[Any] = get_event_loop().create_task( + self.events["on_resync"]({}) # type: ignore + ) + self._tasks_to_close.append(running_task) + + await running_task # Execute resync repeatedly task await resync() diff --git a/port_ocean/exceptions/context.py b/port_ocean/exceptions/context.py index 77283e28a0..9e23f3fd2e 100644 --- a/port_ocean/exceptions/context.py +++ b/port_ocean/exceptions/context.py @@ -15,3 +15,7 @@ class EventContextNotFoundError(NoContextError): class PortOceanContextNotFoundError(NoContextError): pass + + +class PortOceanContextAlreadyInitializedError(NoContextError): + pass diff --git a/port_ocean/exceptions/utils.py b/port_ocean/exceptions/utils.py new file mode 100644 index 0000000000..55f5617ce4 --- /dev/null +++ b/port_ocean/exceptions/utils.py @@ -0,0 +1,9 @@ +from port_ocean.exceptions.core import OceanAbortException + + +class SignalHandlerNotInitialized(OceanAbortException): + pass + + +class SignalHandlerAlreadyInitialized(OceanAbortException): + pass diff --git a/port_ocean/log/__init__.py b/port_ocean/log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/port_ocean/log/handlers.py b/port_ocean/log/handlers.py new file mode 100644 index 0000000000..2c13243e1a --- /dev/null +++ b/port_ocean/log/handlers.py @@ -0,0 +1,88 @@ +import asyncio +import logging +import sys +import threading +import time +from datetime import datetime +from logging.handlers import MemoryHandler +from typing import Any + +from loguru import logger + +from port_ocean import Ocean +from port_ocean.context.ocean import ocean + + +def _serialize_record(record: logging.LogRecord) -> dict[str, Any]: + return { + "message": record.msg, + "level": record.levelname, + "timestamp": datetime.utcfromtimestamp(record.created).strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "extra": record.__dict__["extra"], + } + + +class HTTPMemoryHandler(MemoryHandler): + def __init__( + self, + capacity: int = 100, + flush_level: int = logging.FATAL, + flush_interval: int = 5, + flush_size: int = 1024, + ): + super().__init__(capacity, flushLevel=flush_level, target=None) + self.flush_interval = flush_interval + self.flush_size = flush_size + self.last_flush_time = time.time() + self._serialized_buffer: list[dict[str, Any]] = [] + + @property + def ocean(self) -> Ocean | None: + # We want to wait for the context to be initialized before we can send logs + if ocean.initialized: + return ocean.app + return None + + def emit(self, record: logging.LogRecord) -> None: + self._serialized_buffer.append(_serialize_record(record)) + super().emit(record) + + def shouldFlush(self, record: logging.LogRecord) -> bool: + """ + Extending shouldFlush to include size and time validation as part of the decision whether to flush + """ + if bool(self.buffer) and ( + super(HTTPMemoryHandler, self).shouldFlush(record) + or sys.getsizeof(self.buffer) >= self.flush_size + or time.time() - self.last_flush_time >= self.flush_interval + ): + return True + return False + + def flush(self) -> None: + if self.ocean is None or not self.buffer: + return + + def _wrap_event_loop(_ocean: Ocean, logs_to_send: list[dict[str, Any]]) -> None: + loop = asyncio.new_event_loop() + loop.run_until_complete(self.send_logs(_ocean, logs_to_send)) + loop.close() + + self.acquire() + logs = list(self._serialized_buffer) + if logs: + self.buffer.clear() + self._serialized_buffer.clear() + self.last_flush_time = time.time() + threading.Thread(target=_wrap_event_loop, args=(self.ocean, logs)).start() + self.release() + + async def send_logs( + self, _ocean: Ocean, logs_to_send: list[dict[str, Any]] + ) -> None: + try: + await _ocean.port_client.ingest_integration_logs(logs_to_send) + except Exception as e: + logger.debug(f"Failed to send logs to Port with error: {e}") diff --git a/port_ocean/log/logger_setup.py b/port_ocean/log/logger_setup.py new file mode 100644 index 0000000000..0b171fd96f --- /dev/null +++ b/port_ocean/log/logger_setup.py @@ -0,0 +1,67 @@ +import sys +from logging import LogRecord +from logging.handlers import QueueHandler, QueueListener +from queue import Queue + +import loguru +from loguru import logger + +from port_ocean.config.settings import LogLevelType +from port_ocean.log.handlers import HTTPMemoryHandler +from port_ocean.log.sensetive import sensitive_log_filter + + +def setup_logger(level: LogLevelType, enable_http_handler: bool) -> None: + logger.remove() + _stdout_loguru_handler(level) + if enable_http_handler: + _http_loguru_handler(level) + + +def _stdout_loguru_handler(level: LogLevelType) -> None: + logger_format = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | " + "{level: <8} | " + "{message}" + ) + if level == "DEBUG": + logger_format += " | {extra}" + + logger.add( + sys.stdout, + level=level.upper(), + format=logger_format, + enqueue=True, # process logs in background + diagnose=False, # hide variable values in log backtrace + filter=sensitive_log_filter.create_filter(), + ) + + +def _http_loguru_handler(level: LogLevelType) -> None: + queue: Queue[LogRecord] = Queue() + + handler = QueueHandler(queue) + + logger.add( + handler, + level=level.upper(), + format="{message}", + diagnose=False, # hide variable values in log backtrace + enqueue=True, # process logs in background + filter=sensitive_log_filter.create_filter(full_hide=True), + ) + + queue_listener = QueueListener(queue, HTTPMemoryHandler()) + queue_listener.start() + + +def exception_deserializer(record: "loguru.Record") -> None: + """ + Workaround for when trying to log exception objects with loguru. + Loguru doesn't able to deserialize `Exception` subclasses. + https://github.com/Delgan/loguru/issues/504#issuecomment-917365972 + """ + exception: loguru.RecordException | None = record["exception"] + if exception is not None: + fixed = Exception(str(exception.value)) + record["exception"] = exception._replace(value=fixed) diff --git a/port_ocean/log/sensetive.py b/port_ocean/log/sensetive.py new file mode 100644 index 0000000000..c3c56cb8e3 --- /dev/null +++ b/port_ocean/log/sensetive.py @@ -0,0 +1,51 @@ +import re +from typing import Callable, TYPE_CHECKING + +if TYPE_CHECKING: + from loguru import Record + +# https://github.com/h33tlit/secret-regex-list +secret_patterns = { + "Password in URL": r"[a-zA-Z]{3,10}:\/\/[^\/\\s:@]{3,20}:[^\/\\s:@]{3,20}@.{1,100}[\"'\\s]", + "Generic API Key": r"[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]", + "Generic Secret": r"[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]", + "Google API Key": r"AIza[0-9A-Za-z\\-_]{35}", + "Firebase URL": r".*firebaseio\.com", + "RSA private key": r"-----BEGIN RSA PRIVATE KEY-----", + "SSH (DSA) private key": r"-----BEGIN DSA PRIVATE KEY-----", + "SSH (EC) private key": r"-----BEGIN EC PRIVATE KEY-----", + "PGP private key block": r"-----BEGIN PGP PRIVATE KEY BLOCK-----", + "Amazon AWS Access Key ID": r"AKIA[0-9A-Z]{16}", + "Amazon MWS Auth Token": r"amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "AWS API Key": r"AKIA[0-9A-Z]{16}", + "GitHub": r"[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]", + "Google Cloud Platform API Key": r"AIza[0-9A-Za-z\\-_]{35}", + "Google Cloud Platform OAuth": r"[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com", + "Google (GCP) Service-account": r'"type": "service_account"', + "Google OAuth Access Token": r"ya29\\.[0-9A-Za-z\\-_]+", + "Connection String": r"[a-zA-Z]+:\/\/[^/\s]+:[^/\s]+@[^/\s]+\/[^/\s]+", +} + + +class SensitiveLogFilter: + compiled_patterns = [re.compile(pattern) for pattern in secret_patterns.values()] + + def hide_sensitive_strings(self, *tokens: str) -> None: + self.compiled_patterns.extend([re.compile(token) for token in tokens]) + + def create_filter(self, full_hide: bool = False) -> Callable[["Record"], bool]: + def _filter(record: "Record") -> bool: + for pattern in self.compiled_patterns: + replace: Callable[[re.Match[str]], str] | str = ( + "[REDACTED]" + if full_hide + else lambda match: match.group()[:6] + "[REDACTED]" + ) + record["message"] = pattern.sub(replace, record["message"]) + + return True + + return _filter + + +sensitive_log_filter = SensitiveLogFilter() diff --git a/port_ocean/logger_setup.py b/port_ocean/logger_setup.py deleted file mode 100644 index 7b581e9089..0000000000 --- a/port_ocean/logger_setup.py +++ /dev/null @@ -1,38 +0,0 @@ -import sys - -import loguru -from loguru import logger - -from port_ocean.config.settings import LogLevelType - - -def setup_logger(level: LogLevelType) -> None: - logger_format = ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | " - "{level: <8} | " - "{message}" - ) - if level == "DEBUG": - logger_format += " | {extra}" - - logger.remove() - logger.add( - sys.stdout, - level=level.upper(), - format=logger_format, - enqueue=True, # process logs in background - diagnose=False, # hide variable values in log backtrace - ) - logger.configure(patcher=exception_deserializer) - - -def exception_deserializer(record: "loguru.Record") -> None: - """ - Workaround for when trying to log exception objects with loguru. - loguru doesn't able to deserialize `Exception` subclasses. - https://github.com/Delgan/loguru/issues/504#issuecomment-917365972 - """ - exception: loguru.RecordException | None = record["exception"] - if exception is not None: - fixed = Exception(str(exception.value)) - record["exception"] = exception._replace(value=fixed) diff --git a/port_ocean/middlewares.py b/port_ocean/middlewares.py index 275ad33fd7..cf7c46a078 100644 --- a/port_ocean/middlewares.py +++ b/port_ocean/middlewares.py @@ -6,7 +6,7 @@ from port_ocean.exceptions.api import BaseAPIException, InternalServerException from .context.event import event_context, EventType from .context.ocean import ocean -from .utils import get_time, generate_uuid +from .utils.misc import get_time, generate_uuid async def _handle_silently( diff --git a/port_ocean/ocean.py b/port_ocean/ocean.py index 3640b43f30..24dd4efedd 100644 --- a/port_ocean/ocean.py +++ b/port_ocean/ocean.py @@ -18,8 +18,10 @@ initialize_port_ocean_context, ) from port_ocean.core.integrations.base import BaseIntegration +from port_ocean.log.sensetive import sensitive_log_filter from port_ocean.middlewares import request_handler -from port_ocean.utils import repeat_every +from port_ocean.utils.repeat import repeat_every +from port_ocean.utils.signal import init_signal_handler from port_ocean.version import __integration_version__ @@ -39,10 +41,18 @@ def __init__( self.config = IntegrationConfiguration( base_path="./", **(config_override or {}) ) + if config_factory: - self.config.integration.config = config_factory( - **self.config.integration.config - ).dict() + raw_config = ( + self.config.integration.config + if isinstance(self.config.integration.config, dict) + else self.config.integration.config.dict() + ) + self.config.integration.config = config_factory(**raw_config) + # add the integration sensitive configuration to the sensitive patterns to mask out + sensitive_log_filter.hide_sensitive_strings( + *self.config.get_sensitive_fields_data() + ) self.integration_router = integration_router or APIRouter() self.port_client = PortClient( @@ -61,7 +71,6 @@ async def _setup_scheduled_resync( self, ) -> None: def execute_resync_all() -> None: - initialize_port_ocean_context(ocean_app=self) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -86,11 +95,12 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: @self.fast_api_app.on_event("startup") async def startup() -> None: + init_signal_handler() try: await self.integration.start() await self._setup_scheduled_resync() - except Exception as e: - logger.error(f"Failed to start integration with error: {e}") + except Exception: + logger.exception("Failed to start integration") sys.exit("Server stopped") await self.fast_api_app(scope, receive, send) diff --git a/port_ocean/run.py b/port_ocean/run.py index 24084a1348..bb3c7c218c 100644 --- a/port_ocean/run.py +++ b/port_ocean/run.py @@ -1,16 +1,16 @@ from inspect import getmembers -from typing import Type, Dict, Any -from pydantic import BaseModel +from typing import Dict, Any, Type import uvicorn +from pydantic import BaseModel from port_ocean.bootstrap import create_default_app from port_ocean.config.dynamic import default_config_factory from port_ocean.config.settings import ApplicationSettings, LogLevelType from port_ocean.core.defaults.initialize import initialize_defaults -from port_ocean.logger_setup import setup_logger +from port_ocean.log.logger_setup import setup_logger from port_ocean.ocean import Ocean -from port_ocean.utils import get_spec_file, load_module +from port_ocean.utils.misc import get_spec_file, load_module def _get_default_config_factory() -> None | Type[BaseModel]: @@ -31,7 +31,11 @@ def run( ) -> None: application_settings = ApplicationSettings(log_level=log_level, port=port) - setup_logger(application_settings.log_level) + setup_logger( + application_settings.log_level, + enable_http_handler=application_settings.enable_http_logging, + ) + config_factory = _get_default_config_factory() default_app = create_default_app(path, config_factory, config_override) diff --git a/port_ocean/utils/__init__.py b/port_ocean/utils/__init__.py new file mode 100644 index 0000000000..25c8de8f43 --- /dev/null +++ b/port_ocean/utils/__init__.py @@ -0,0 +1,3 @@ +from port_ocean.utils.async_http import http_async_client + +__all__ = ["http_async_client"] diff --git a/port_ocean/utils/async_http.py b/port_ocean/utils/async_http.py new file mode 100644 index 0000000000..a96c497a41 --- /dev/null +++ b/port_ocean/utils/async_http.py @@ -0,0 +1,29 @@ +import httpx +from werkzeug.local import LocalStack, LocalProxy + +from port_ocean.context.ocean import ocean +from port_ocean.helpers.async_client import OceanAsyncClient +from port_ocean.helpers.retry import RetryTransport + +_http_client: LocalStack[httpx.AsyncClient] = LocalStack() + + +def _get_http_client_context() -> httpx.AsyncClient: + client = _http_client.top + if client is None: + client = OceanAsyncClient(RetryTransport, timeout=ocean.config.client_timeout) + _http_client.push(client) + + return client + + +""" +Utilize this client for all outbound integration requests to the third-party application. It functions as a wrapper +around the httpx.AsyncClient, incorporating retry logic at the transport layer for handling retries on 5xx errors and +connection errors. + +The client is instantiated lazily, only coming into existence upon its initial access. It should not be closed when in +use, as it operates as a singleton shared across all events in the thread. It also takes care of recreating the client +in scenarios such as the creation of a new event loop, such as when initiating a new thread. +""" +http_async_client: httpx.AsyncClient = LocalProxy(lambda: _get_http_client_context()) # type: ignore diff --git a/port_ocean/utils/misc.py b/port_ocean/utils/misc.py new file mode 100644 index 0000000000..c0999a1b76 --- /dev/null +++ b/port_ocean/utils/misc.py @@ -0,0 +1,56 @@ +import inspect +from importlib.util import spec_from_file_location, module_from_spec +from pathlib import Path +from time import time +from types import ModuleType +from typing import Callable, Any +from uuid import uuid4 + +import tomli +import yaml + + +def get_time(seconds_precision: bool = True) -> float: + """Return current time as Unix/Epoch timestamp, in seconds. + :param seconds_precision: if True, return with seconds precision as integer (default). + If False, return with milliseconds precision as floating point number of seconds. + """ + return time() if not seconds_precision else int(time()) + + +def generate_uuid() -> str: + """Return a UUID4 as string""" + return str(uuid4()) + + +def get_function_location(func: Callable[..., Any]) -> str: + file_path = inspect.getsourcefile(func) + line_number = inspect.getsourcelines(func)[1] + return f"{file_path}:{line_number}" + + +def get_integration_version() -> str: + try: + with open("./pyproject.toml", "rb") as toml_file: + pyproject_data = tomli.load(toml_file) + return pyproject_data["tool"]["poetry"]["version"] + except (FileNotFoundError, KeyError): + return "" + + +def get_spec_file(path: Path = Path(".")) -> dict[str, Any] | None: + try: + return yaml.safe_load((path / ".port/spec.yaml").read_text()) + except FileNotFoundError: + return None + + +def load_module(file_path: str) -> ModuleType: + spec = spec_from_file_location("module.name", file_path) + if spec is None or spec.loader is None: + raise Exception(f"Failed to load integration from path: {file_path}") + + module = module_from_spec(spec) + spec.loader.exec_module(module) + + return module diff --git a/port_ocean/utils.py b/port_ocean/utils/repeat.py similarity index 51% rename from port_ocean/utils.py rename to port_ocean/utils/repeat.py index 5c64de70cf..587237e45b 100644 --- a/port_ocean/utils.py +++ b/port_ocean/utils/repeat.py @@ -1,95 +1,11 @@ import asyncio -import inspect from asyncio import ensure_future from functools import wraps -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path -from time import time from traceback import format_exception -from types import ModuleType -from typing import Callable, Any, Coroutine -from uuid import uuid4 +from typing import Callable, Coroutine, Any -import httpx -import tomli -import yaml from loguru import logger from starlette.concurrency import run_in_threadpool -from werkzeug.local import LocalStack, LocalProxy - -from port_ocean.helpers.async_client import OceanAsyncClient -from port_ocean.context.ocean import ocean -from port_ocean.helpers.retry import RetryTransport - -_http_client: LocalStack[httpx.AsyncClient] = LocalStack() - - -def _get_http_client_context() -> httpx.AsyncClient: - client = _http_client.top - if client is None: - client = OceanAsyncClient(RetryTransport, timeout=ocean.config.client_timeout) - _http_client.push(client) - - return client - - -""" -Utilize this client for all outbound integration requests to the third-party application. It functions as a wrapper -around the httpx.AsyncClient, incorporating retry logic at the transport layer for handling retries on 5xx errors and -connection errors. - -The client is instantiated lazily, only coming into existence upon its initial access. It should not be closed when in -use, as it operates as a singleton shared across all events in the thread. It also takes care of recreating the client -in scenarios such as the creation of a new event loop, such as when initiating a new thread. -""" -http_async_client: httpx.AsyncClient = LocalProxy(lambda: _get_http_client_context()) # type: ignore - - -def get_time(seconds_precision: bool = True) -> float: - """Return current time as Unix/Epoch timestamp, in seconds. - :param seconds_precision: if True, return with seconds precision as integer (default). - If False, return with milliseconds precision as floating point number of seconds. - """ - return time() if not seconds_precision else int(time()) - - -def generate_uuid() -> str: - """Return a UUID4 as string""" - return str(uuid4()) - - -def get_function_location(func: Callable[..., Any]) -> str: - file_path = inspect.getsourcefile(func) - line_number = inspect.getsourcelines(func)[1] - return f"{file_path}:{line_number}" - - -def get_integration_version() -> str: - try: - with open("./pyproject.toml", "rb") as toml_file: - pyproject_data = tomli.load(toml_file) - return pyproject_data["tool"]["poetry"]["version"] - except (FileNotFoundError, KeyError): - return "" - - -def get_spec_file(path: Path = Path(".")) -> dict[str, Any] | None: - try: - return yaml.safe_load((path / ".port/spec.yaml").read_text()) - except FileNotFoundError: - return None - - -def load_module(file_path: str) -> ModuleType: - spec = spec_from_file_location("module.name", file_path) - if spec is None or spec.loader is None: - raise Exception(f"Failed to load integration from path: {file_path}") - - module = module_from_spec(spec) - spec.loader.exec_module(module) - - return module - NoArgsNoReturnFuncT = Callable[[], None] NoArgsNoReturnAsyncFuncT = Callable[[], Coroutine[Any, Any, None]] diff --git a/port_ocean/utils/signal.py b/port_ocean/utils/signal.py new file mode 100644 index 0000000000..ffab884903 --- /dev/null +++ b/port_ocean/utils/signal.py @@ -0,0 +1,54 @@ +import signal +from typing import Callable, Any + +from werkzeug.local import LocalProxy, LocalStack + +from port_ocean.exceptions.utils import ( + SignalHandlerNotInitialized, + SignalHandlerAlreadyInitialized, +) +from port_ocean.utils.misc import generate_uuid + + +class SignalHandler: + def __init__(self) -> None: + self._handlers: dict[str, Callable[[], Any]] = {} + signal.signal(signal.SIGINT, lambda _, __: self._exit()) + signal.signal(signal.SIGTERM, lambda _, __: self._exit()) + + def _exit(self) -> None: + """ + Handles the exit signal. + """ + while self._handlers: + _, handler = self._handlers.popitem() + handler() + + def register(self, callback: Callable[[], Any]) -> str: + _id = generate_uuid() + self._handlers[_id] = callback + + return _id + + def unregister(self, _id: str) -> None: + del self._handlers[_id] + + +_signal_handler: LocalStack[SignalHandler] = LocalStack() + + +def _get_signal_handler() -> SignalHandler: + global _signal_handler + if _signal_handler.top is None: + raise SignalHandlerNotInitialized("Signal handler is not initialized") + return _signal_handler.top + + +signal_handler: SignalHandler = LocalProxy(_get_signal_handler) # type: ignore + + +def init_signal_handler() -> None: + global _signal_handler + if _signal_handler.top is not None: + raise SignalHandlerAlreadyInitialized("Signal handler is already initialized") + _signal_handler.push(SignalHandler()) diff --git a/port_ocean/version.py b/port_ocean/version.py index 45ce1ad16d..182d840b53 100644 --- a/port_ocean/version.py +++ b/port_ocean/version.py @@ -1,6 +1,6 @@ from importlib.metadata import version -from .utils import get_integration_version +from .utils.misc import get_integration_version __version__ = version("port-ocean") __integration_version__ = get_integration_version() diff --git a/pyproject.toml b/pyproject.toml index bb9631c12c..16099ae9fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "port-ocean" -version = "0.4.17" +version = "0.5.0" description = "Port Ocean is a CLI tool for managing your Port projects." readme = "README.md" homepage = "https://app.getport.io"