From 53e8d6c8ae84e72d17b620a136101d6accd24a31 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 10 Jul 2025 20:11:28 +0100 Subject: [PATCH 1/7] feat: Support SDK metrics --- flagsmith/__init__.py | 7 +-- flagsmith/flagsmith.py | 31 +++++++++++++- flagsmith/types.py | 7 ++- flagsmith/version.py | 3 ++ tests/test_flagsmith.py | 95 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 flagsmith/version.py diff --git a/flagsmith/__init__.py b/flagsmith/__init__.py index f576701..41473e4 100644 --- a/flagsmith/__init__.py +++ b/flagsmith/__init__.py @@ -1,4 +1,5 @@ -from . import webhooks -from .flagsmith import Flagsmith +from flagsmith import webhooks +from flagsmith.flagsmith import Flagsmith +from flagsmith.version import __version__ -__all__ = ("Flagsmith", "webhooks") +__all__ = ("Flagsmith", "webhooks", "__version__") diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index 1d05fcc..db4b3a9 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -11,6 +11,7 @@ from flag_engine.identities.traits.types import TraitValue from flag_engine.segments.evaluator import get_identity_segments from requests.adapters import HTTPAdapter +from requests.utils import default_user_agent from urllib3 import Retry from flagsmith.analytics import AnalyticsProcessor @@ -19,13 +20,15 @@ from flagsmith.offline_handlers import BaseOfflineHandler from flagsmith.polling_manager import EnvironmentDataPollingManager from flagsmith.streaming_manager import EventStreamManager, StreamEvent -from flagsmith.types import JsonType, TraitConfig, TraitMapping +from flagsmith.types import ApplicationMetadata, JsonType, TraitConfig, TraitMapping from flagsmith.utils.identities import generate_identity_data +from flagsmith.version import __version__ logger = logging.getLogger(__name__) DEFAULT_API_URL = "https://edge.api.flagsmith.com/api/v1/" DEFAULT_REALTIME_API_URL = "https://realtime.flagsmith.com/" +DEFAULT_USER_AGENT = f"flagsmith-python-client/{__version__} " + default_user_agent() class Flagsmith: @@ -61,6 +64,7 @@ def __init__( offline_mode: bool = False, offline_handler: typing.Optional[BaseOfflineHandler] = None, enable_realtime_updates: bool = False, + application_metadata: typing.Optional[ApplicationMetadata] = None, ): """ :param environment_key: The environment key obtained from Flagsmith interface. @@ -88,6 +92,7 @@ def __init__( document from another source when in offline_mode. Works in place of default_flag_handler if offline_mode is not set and using remote evaluation. :param enable_realtime_updates: Use real-time functionality via SSE as opposed to polling the API + :param application_metadata: Optional metadata about the client application. """ self.offline_mode = offline_mode @@ -122,7 +127,11 @@ def __init__( self.session = requests.Session() self.session.headers.update( - **{"X-Environment-Key": environment_key}, **(custom_headers or {}) + self._get_headers( + environment_key=environment_key, + application_metadata=application_metadata, + custom_headers=custom_headers, + ) ) self.session.proxies.update(proxies or {}) retries = retries or Retry(total=3, backoff_factor=0.1) @@ -275,6 +284,24 @@ def update_environment(self) -> None: identity.identifier: identity for identity in overrides } + def _get_headers( + self, + environment_key: str, + application_metadata: ApplicationMetadata, + custom_headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + ) -> typing.Dict[str, str]: + headers = { + "X-Environment-Key": environment_key, + "User-Agent": DEFAULT_USER_AGENT, + } + if application_metadata: + if name := application_metadata.get("name"): + headers["Flagsmith-Application-Name"] = name + if version := application_metadata.get("version"): + headers["Flagsmith-Application-Version"] = version + headers.update(custom_headers or {}) + return headers + def _get_environment_from_api(self) -> EnvironmentModel: environment_data = self._get_json_response(self.environment_url, method="GET") return EnvironmentModel.model_validate(environment_data) diff --git a/flagsmith/types.py b/flagsmith/types.py index b2a41a3..1681a44 100644 --- a/flagsmith/types.py +++ b/flagsmith/types.py @@ -1,7 +1,7 @@ import typing from flag_engine.identities.traits.types import TraitValue -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, NotRequired _JsonScalarType: TypeAlias = typing.Union[ int, @@ -23,3 +23,8 @@ class TraitConfig(typing.TypedDict): TraitMapping: TypeAlias = typing.Mapping[str, typing.Union[TraitValue, TraitConfig]] + + +class ApplicationMetadata(typing.TypedDict): + name: NotRequired[str] + version: NotRequired[str] diff --git a/flagsmith/version.py b/flagsmith/version.py new file mode 100644 index 0000000..aa2c58c --- /dev/null +++ b/flagsmith/version.py @@ -0,0 +1,3 @@ +from importlib.metadata import version + +__version__ = version("flagsmith") diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 71f3a1b..9bc0892 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -11,7 +11,7 @@ from pytest_mock import MockerFixture from responses import matchers -from flagsmith import Flagsmith +from flagsmith import Flagsmith, __version__ from flagsmith.exceptions import ( FlagsmithAPIError, FlagsmithFeatureDoesNotExistError, @@ -717,3 +717,96 @@ def test_custom_feature_error_raised_when_invalid_feature( with pytest.raises(FlagsmithFeatureDoesNotExistError): # When flags.is_feature_enabled("non-existing-feature") + + +@pytest.fixture +def default_headers() -> typing.Dict[str, str]: + return { + "User-Agent": f"flagsmith-python-client/{__version__} python-requests/2.32.4", + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*", + "Connection": "keep-alive", + } + + +@pytest.mark.parametrize( + "kwargs,expected_headers", + [ + ( + { + "environment_key": "test-key", + "application_metadata": {"name": "test-app", "version": "1.0.0"}, + }, + { + "Flagsmith-Application-Name": "test-app", + "Flagsmith-Application-Version": "1.0.0", + "X-Environment-Key": "test-key", + }, + ), + ( + { + "environment_key": "test-key", + "application_metadata": {"name": "test-app"}, + }, + { + "Flagsmith-Application-Name": "test-app", + "X-Environment-Key": "test-key", + }, + ), + ( + { + "environment_key": "test-key", + "application_metadata": {"version": "1.0.0"}, + }, + { + "Flagsmith-Application-Version": "1.0.0", + "X-Environment-Key": "test-key", + }, + ), + ( + { + "environment_key": "test-key", + "application_metadata": {"version": "1.0.0"}, + "custom_headers": {"X-Custom-Header": "CustomValue"}, + }, + { + "Flagsmith-Application-Version": "1.0.0", + "X-Environment-Key": "test-key", + "X-Custom-Header": "CustomValue", + }, + ), + ( + { + "environment_key": "test-key", + "application_metadata": None, + "custom_headers": {"X-Custom-Header": "CustomValue"}, + }, + { + "X-Environment-Key": "test-key", + "X-Custom-Header": "CustomValue", + }, + ), + ( + {"environment_key": "test-key"}, + { + "X-Environment-Key": "test-key", + }, + ), + ], +) +@responses.activate() +def test_flagsmith__init__expected_headers_sent( + default_headers: typing.Dict[str, str], + kwargs: typing.Dict[str, typing.Any], + expected_headers: typing.Dict[str, str], +) -> None: + # Given + flagsmith = Flagsmith(**kwargs) + responses.add(method="GET", url=flagsmith.environment_flags_url, body="{}") + + # When + flagsmith.get_environment_flags() + + # Then + headers = responses.calls[0].request.headers + assert headers == {**default_headers, **expected_headers} From 63f8cbe5f787f40703e54094deac06d02fd4bfe5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 10 Jul 2025 20:19:34 +0100 Subject: [PATCH 2/7] fix import order --- flagsmith/flagsmith.py | 7 ++++++- flagsmith/types.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index db4b3a9..ea0a764 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -20,7 +20,12 @@ from flagsmith.offline_handlers import BaseOfflineHandler from flagsmith.polling_manager import EnvironmentDataPollingManager from flagsmith.streaming_manager import EventStreamManager, StreamEvent -from flagsmith.types import ApplicationMetadata, JsonType, TraitConfig, TraitMapping +from flagsmith.types import ( + ApplicationMetadata, + JsonType, + TraitConfig, + TraitMapping, +) from flagsmith.utils.identities import generate_identity_data from flagsmith.version import __version__ diff --git a/flagsmith/types.py b/flagsmith/types.py index 1681a44..c0d535a 100644 --- a/flagsmith/types.py +++ b/flagsmith/types.py @@ -1,7 +1,7 @@ import typing from flag_engine.identities.traits.types import TraitValue -from typing_extensions import TypeAlias, NotRequired +from typing_extensions import NotRequired, TypeAlias _JsonScalarType: TypeAlias = typing.Union[ int, From 3c9832f854ffc0e2313b078f9420a35f1afa705e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 10 Jul 2025 23:17:41 +0100 Subject: [PATCH 3/7] fix typing --- flagsmith/flagsmith.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index ea0a764..3452f20 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -292,8 +292,8 @@ def update_environment(self) -> None: def _get_headers( self, environment_key: str, - application_metadata: ApplicationMetadata, - custom_headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + application_metadata: typing.Optional[ApplicationMetadata], + custom_headers: typing.Optional[typing.Dict[str, typing.Any]], ) -> typing.Dict[str, str]: headers = { "X-Environment-Key": environment_key, From 7c64a9f59bb51bfd3ca7f3b2c985e4b464fb3e5f Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 21 Jul 2025 18:20:51 +0100 Subject: [PATCH 4/7] add python version --- flagsmith/flagsmith.py | 7 ++++++- tests/test_flagsmith.py | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index 3452f20..5ed7404 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -1,4 +1,5 @@ import logging +import sys import typing from datetime import timezone @@ -33,7 +34,11 @@ DEFAULT_API_URL = "https://edge.api.flagsmith.com/api/v1/" DEFAULT_REALTIME_API_URL = "https://realtime.flagsmith.com/" -DEFAULT_USER_AGENT = f"flagsmith-python-client/{__version__} " + default_user_agent() +DEFAULT_USER_AGENT = ( + f"flagsmith-python-client/{__version__} " + + default_user_agent() + + f" python/{sys.version_info.major}.{sys.version_info.minor}" +) class Flagsmith: diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 9bc0892..4f6e5ff 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -1,4 +1,5 @@ import json +import sys import time import typing import uuid @@ -722,7 +723,10 @@ def test_custom_feature_error_raised_when_invalid_feature( @pytest.fixture def default_headers() -> typing.Dict[str, str]: return { - "User-Agent": f"flagsmith-python-client/{__version__} python-requests/2.32.4", + "User-Agent": ( + f"flagsmith-python-client/{__version__} python-requests/{requests.__version__} " + f"python/{sys.version_info.major}.{sys.version_info.minor}" + ), "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive", From f0607a1a5c1e9e816b6fad3f0b077d90693ca809 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 21 Jul 2025 20:03:49 +0100 Subject: [PATCH 5/7] move typecheck to CI, bump pre-commit --- .github/workflows/pytest.yml | 11 +++ .pre-commit-config.yaml | 7 -- poetry.lock | 153 ++++++++++++++++++++++++++++++++--- pyproject.toml | 3 +- 4 files changed, 154 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 743f3a7..1233e71 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -32,5 +32,16 @@ jobs: pip install poetry poetry install --with dev + # mypy doesn't support Python 3.8, so we run it only for 3.9 and above. + # Still check for typing errors for Python 3.8 when running against one of the newer versions. + # See https://mypy.readthedocs.io/en/stable/changelog.html#drop-support-for-python-3-8. + - name: Check for new typing errors for Python 3.8 + if: ${{ matrix.python-version == '3.9' }} + run: poetry run mypy --strict --python-version 3.8 . + + - name: Check for new typing errors + if: ${{ matrix.python-version != '3.8' }} + run: poetry run mypy --strict . + - name: Run Tests run: poetry run pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd1c372..f295f10 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,4 @@ repos: - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 - hooks: - - id: mypy - args: [--strict] - additional_dependencies: - [pydantic, pytest, pytest_mock, types-requests, flagsmith-flag-engine, responses, sseclient-py] - repo: https://github.com/PyCQA/isort rev: 6.0.1 hooks: diff --git a/poetry.lock b/poetry.lock index 8b2bdd8..d5b1228 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -20,6 +21,7 @@ version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, @@ -31,6 +33,8 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -42,6 +46,7 @@ version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, @@ -141,6 +146,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -152,6 +159,7 @@ version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, @@ -231,7 +239,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "distlib" @@ -239,6 +247,8 @@ version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, @@ -250,6 +260,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -264,6 +276,8 @@ version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, @@ -272,7 +286,7 @@ files = [ [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""] [[package]] name = "flagsmith-flag-engine" @@ -280,6 +294,7 @@ version = "5.3.1" description = "Flag engine for the Flagsmith API." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "flagsmith-flag-engine-5.3.1.tar.gz", hash = "sha256:6f04cd3f8dc8ffed0454d43dc980ac521d52637e8f107eed3dadcfb5a6ee233f"}, ] @@ -295,6 +310,8 @@ version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, @@ -309,6 +326,7 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main", "dev"] files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -320,17 +338,89 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "mypy" +version = "1.17.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\"" +files = [ + {file = "mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6"}, + {file = "mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d"}, + {file = "mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b"}, + {file = "mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a"}, + {file = "mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f"}, + {file = "mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937"}, + {file = "mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be"}, + {file = "mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61"}, + {file = "mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f"}, + {file = "mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d"}, + {file = "mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3"}, + {file = "mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70"}, + {file = "mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb"}, + {file = "mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d"}, + {file = "mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8"}, + {file = "mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e"}, + {file = "mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8"}, + {file = "mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d"}, + {file = "mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06"}, + {file = "mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a"}, + {file = "mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889"}, + {file = "mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba"}, + {file = "mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658"}, + {file = "mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c"}, + {file = "mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab"}, + {file = "mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad"}, + {file = "mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c"}, + {file = "mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8"}, + {file = "mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97"}, + {file = "mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4"}, + {file = "mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496"}, + {file = "mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.9\"" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + [[package]] name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -342,17 +432,33 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.9\"" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, @@ -369,6 +475,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -380,13 +487,15 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.21.0" +version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ - {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, - {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [package.dependencies] @@ -402,6 +511,7 @@ version = "2.8.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, @@ -424,6 +534,7 @@ version = "0.6.0" description = "Collections of pydantic models" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pydantic_collections-0.6.0-py3-none-any.whl", hash = "sha256:ec559722abf6a0f80e6f00b3d28f0f39c0ed5feb1641166230eb75e9da880162"}, {file = "pydantic_collections-0.6.0.tar.gz", hash = "sha256:c34d3fd1df5600b315cdecdd8e74eacd4c8c607b7e3f2c9392b2a15850a4ef9e"}, @@ -439,6 +550,7 @@ version = "2.20.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, @@ -540,6 +652,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -562,6 +675,7 @@ version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, @@ -580,6 +694,7 @@ version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -597,6 +712,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -659,6 +775,7 @@ version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -680,6 +797,7 @@ version = "1.0.1" description = "Asynchronous Python HTTP for Humans." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "requests-futures-1.0.1.tar.gz", hash = "sha256:f55a4ef80070e2858e7d1e73123d2bfaeaf25b93fd34384d8ddf148e2b676373"}, {file = "requests_futures-1.0.1-py2.py3-none-any.whl", hash = "sha256:4a2f5472e9911a79532137d156aa937cd9cd90fec55677f71b2976d1f7a66d38"}, @@ -697,6 +815,7 @@ version = "0.24.1" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "responses-0.24.1-py3-none-any.whl", hash = "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9"}, {file = "responses-0.24.1.tar.gz", hash = "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c"}, @@ -708,7 +827,7 @@ requests = ">=2.30.0,<3.0" urllib3 = ">=1.25.10,<3.0" [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] [[package]] name = "semver" @@ -716,6 +835,7 @@ version = "3.0.2" description = "Python helper for Semantic Versioning (https://semver.org)" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, @@ -727,6 +847,7 @@ version = "1.8.0" description = "SSE client for Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "sseclient-py-1.8.0.tar.gz", hash = "sha256:c547c5c1a7633230a38dc599a21a2dc638f9b5c297286b48b46b935c71fac3e8"}, {file = "sseclient_py-1.8.0-py2.py3-none-any.whl", hash = "sha256:4ecca6dc0b9f963f8384e9d7fd529bf93dd7d708144c4fb5da0e0a1a926fee83"}, @@ -738,6 +859,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -749,6 +872,7 @@ version = "2.32.0.20240712" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-requests-2.32.0.20240712.tar.gz", hash = "sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358"}, {file = "types_requests-2.32.0.20240712-py3-none-any.whl", hash = "sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"}, @@ -763,10 +887,12 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +markers = {dev = "python_version >= \"3.9\""} [[package]] name = "urllib3" @@ -774,13 +900,14 @@ version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -791,6 +918,8 @@ version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version >= \"3.9\"" files = [ {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, @@ -803,9 +932,9 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.8.1,<4" -content-hash = "4da7829338a4d12130b2c2b77dc2804032bab4458a6d95624faeaf64433c7516" +content-hash = "4e63cf3ee6115e58105337de9da451766f8381bf707134ed383848d4dbeae454" diff --git a/pyproject.toml b/pyproject.toml index 1cf9c6d..e09349c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,10 +21,11 @@ pydantic = "^2" optional = true [tool.poetry.group.dev.dependencies] +mypy = { version = "^1.16.1", python = ">=3.9,<4" } pytest = "^7.4.0" pytest-cov = "^4.1.0" pytest-mock = "^3.6.1" -pre-commit = "^2.17.0" +pre-commit = { version = "^4.2.0", python = ">=3.9,<4" } responses = "^0.24.1" types-requests = "^2.32" From 90f0cd69fb2c302c3574dd39d84041e5ccbfcc1d Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 21 Jul 2025 20:10:13 +0100 Subject: [PATCH 6/7] explicit default headers --- tests/test_flagsmith.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 4f6e5ff..9ad1c84 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -720,19 +720,6 @@ def test_custom_feature_error_raised_when_invalid_feature( flags.is_feature_enabled("non-existing-feature") -@pytest.fixture -def default_headers() -> typing.Dict[str, str]: - return { - "User-Agent": ( - f"flagsmith-python-client/{__version__} python-requests/{requests.__version__} " - f"python/{sys.version_info.major}.{sys.version_info.minor}" - ), - "Accept-Encoding": "gzip, deflate", - "Accept": "*/*", - "Connection": "keep-alive", - } - - @pytest.mark.parametrize( "kwargs,expected_headers", [ @@ -800,7 +787,6 @@ def default_headers() -> typing.Dict[str, str]: ) @responses.activate() def test_flagsmith__init__expected_headers_sent( - default_headers: typing.Dict[str, str], kwargs: typing.Dict[str, typing.Any], expected_headers: typing.Dict[str, str], ) -> None: @@ -813,4 +799,13 @@ def test_flagsmith__init__expected_headers_sent( # Then headers = responses.calls[0].request.headers - assert headers == {**default_headers, **expected_headers} + assert headers == { + "User-Agent": ( + f"flagsmith-python-client/{__version__} python-requests/{requests.__version__} " + f"python/{sys.version_info.major}.{sys.version_info.minor}" + ), + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*", + "Connection": "keep-alive", + **expected_headers, + } From 23166545903ecb4d800ba141b5185d07074db5ec Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 21 Jul 2025 20:14:58 +0100 Subject: [PATCH 7/7] drop 3.8 --- .github/workflows/pytest.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1233e71..8cc83d3 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -32,13 +32,6 @@ jobs: pip install poetry poetry install --with dev - # mypy doesn't support Python 3.8, so we run it only for 3.9 and above. - # Still check for typing errors for Python 3.8 when running against one of the newer versions. - # See https://mypy.readthedocs.io/en/stable/changelog.html#drop-support-for-python-3-8. - - name: Check for new typing errors for Python 3.8 - if: ${{ matrix.python-version == '3.9' }} - run: poetry run mypy --strict --python-version 3.8 . - - name: Check for new typing errors if: ${{ matrix.python-version != '3.8' }} run: poetry run mypy --strict .