diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 193b100..83d4823 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -1,6 +1,6 @@ name: Unit Tests -on: [ push ] +on: [push] jobs: unit_tests: @@ -8,7 +8,7 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - python-version: [ "3.12" ] + python-version: ["3.12", "3.9"] os: [ubuntu-latest, windows-latest] steps: @@ -17,11 +17,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' + cache: "pip" - name: Install dependencies run: | pip install --upgrade pip pip install -r requirements.test.txt - name: Run unit tests run: | - pytest \ No newline at end of file + pytest diff --git a/.gitignore b/.gitignore index a2933c8..fd5ac2d 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,9 @@ target/ benchmark.json pip-wheel-metadata/ -.venv/ \ No newline at end of file +.venv/ + +# Virtual environments +venv*/ +.venv*/ +*venv/ \ No newline at end of file diff --git a/README.md b/README.md index ad22f7c..c537f3f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This SDK allows your application to interface with the [DevCycle Bucketing API]( ## Requirements -* Python 3.8+ +* Python 3.9+ ## Installation diff --git a/devcycle_python_sdk/bucketing-lib.debug.wasm b/devcycle_python_sdk/bucketing-lib.debug.wasm deleted file mode 100644 index a2d755a..0000000 Binary files a/devcycle_python_sdk/bucketing-lib.debug.wasm and /dev/null differ diff --git a/devcycle_python_sdk/bucketing-lib.release.wasm b/devcycle_python_sdk/bucketing-lib.release.wasm index 348b8bd..5eb145f 100644 Binary files a/devcycle_python_sdk/bucketing-lib.release.wasm and b/devcycle_python_sdk/bucketing-lib.release.wasm differ diff --git a/devcycle_python_sdk/cloud_client.py b/devcycle_python_sdk/cloud_client.py index d1140e4..a7591e6 100644 --- a/devcycle_python_sdk/cloud_client.py +++ b/devcycle_python_sdk/cloud_client.py @@ -182,6 +182,13 @@ def track(self, user: DevCycleUser, user_event: DevCycleEvent) -> None: except Exception as e: logger.error(f"DevCycle: Error tracking event: {e}") + def close(self) -> None: + """ + Closes the client and releases any resources held by it. + """ + # Cloud client doesn't need to release any resources + logger.debug("DevCycle: Cloud client closed") + def _validate_sdk_key(sdk_key: str) -> None: if sdk_key is None or len(sdk_key) == 0: diff --git a/devcycle_python_sdk/devcycle_client.py b/devcycle_python_sdk/devcycle_client.py index a488de7..7fc6863 100644 --- a/devcycle_python_sdk/devcycle_client.py +++ b/devcycle_python_sdk/devcycle_client.py @@ -38,3 +38,10 @@ def get_openfeature_provider(self) -> AbstractProvider: @abstractmethod def get_sdk_platform(self) -> str: pass + + @abstractmethod + def close(self) -> None: + """ + Closes the client and releases any resources held by it. + """ + pass diff --git a/devcycle_python_sdk/local_client.py b/devcycle_python_sdk/local_client.py index 4aa55b9..a41f4b9 100644 --- a/devcycle_python_sdk/local_client.py +++ b/devcycle_python_sdk/local_client.py @@ -2,7 +2,7 @@ import logging import uuid from numbers import Real -from typing import Any, Dict, Union +from typing import Any, Dict, Union, Optional from devcycle_python_sdk import DevCycleLocalOptions, AbstractDevCycleClient from devcycle_python_sdk.api.local_bucketing import LocalBucketing @@ -50,12 +50,21 @@ def __init__(self, sdk_key: str, options: DevCycleLocalOptions): sdk_key, self.client_uuid, self.options, self.local_bucketing ) - self._openfeature_provider = DevCycleProvider(self) + self._openfeature_provider: Optional[DevCycleProvider] = None def get_sdk_platform(self) -> str: return "Local" def get_openfeature_provider(self) -> AbstractProvider: + if self._openfeature_provider is None: + self._openfeature_provider = DevCycleProvider(self) + + # Update platform data for OpenFeature + self._platform_data.sdkPlatform = "python-of" + self.local_bucketing.set_platform_data( + json.dumps(self._platform_data.to_json()) + ) + return self._openfeature_provider def is_initialized(self) -> bool: diff --git a/devcycle_python_sdk/models/event.py b/devcycle_python_sdk/models/event.py index 20b0563..64cc40d 100644 --- a/devcycle_python_sdk/models/event.py +++ b/devcycle_python_sdk/models/event.py @@ -18,7 +18,7 @@ class EventType: class DevCycleEvent: type: Optional[str] = None target: Optional[str] = None - date: datetime = field(default_factory=lambda: datetime.utcnow()) + date: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) value: Optional[int] = None metaData: Optional[Dict[str, str]] = None diff --git a/devcycle_python_sdk/models/platform_data.py b/devcycle_python_sdk/models/platform_data.py index 3e44ca2..d111098 100644 --- a/devcycle_python_sdk/models/platform_data.py +++ b/devcycle_python_sdk/models/platform_data.py @@ -1,6 +1,7 @@ # ruff: noqa: N815 import platform import socket +from typing import Optional from dataclasses import dataclass from devcycle_python_sdk.util.version import sdk_version @@ -13,6 +14,7 @@ class PlatformData: deviceModel: str platform: str hostname: str + sdkPlatform: Optional[str] = None def to_json(self): return { diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index 88692d6..57d4c56 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -16,7 +16,7 @@ class DevCycleUser: appVersion: Optional[str] = None appBuild: Optional[str] = None customData: Optional[Dict[str, Any]] = None - createdDate: datetime = field(default_factory=lambda: datetime.utcnow()) + createdDate: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) privateCustomData: Optional[Dict[str, Any]] = None lastSeenDate: Optional[datetime] = None platform: Optional[str] = None @@ -24,6 +24,7 @@ class DevCycleUser: deviceModel: Optional[str] = None sdkType: Optional[str] = None sdkVersion: Optional[str] = None + sdkPlatform: Optional[str] = None def to_json(self): json_dict = { @@ -50,7 +51,7 @@ def from_json(cls, data: dict) -> "DevCycleUser": data["createdDate"].replace("Z", "+00:00") ) else: - created_date = datetime.utcnow() + created_date = datetime.now(timezone.utc) last_seen_date = None if "lastSeenDate" in data: @@ -75,6 +76,7 @@ def from_json(cls, data: dict) -> "DevCycleUser": deviceModel=data.get("deviceModel"), sdkType=data.get("sdkType"), sdkVersion=data.get("sdkVersion"), + sdkPlatform=data.get("sdkPlatform"), ) @staticmethod @@ -156,4 +158,5 @@ def create_user_from_context( if private_custom_data: user.privateCustomData = private_custom_data + user.sdkPlatform = "python-of" return user diff --git a/devcycle_python_sdk/open_feature_provider/provider.py b/devcycle_python_sdk/open_feature_provider/provider.py index 9911e77..8de69de 100644 --- a/devcycle_python_sdk/open_feature_provider/provider.py +++ b/devcycle_python_sdk/open_feature_provider/provider.py @@ -1,4 +1,5 @@ import logging +import time from typing import Any, Optional, Union, List @@ -6,11 +7,16 @@ from devcycle_python_sdk.models.user import DevCycleUser from openfeature.provider import AbstractProvider +from openfeature.provider.metadata import Metadata from openfeature.evaluation_context import EvaluationContext from openfeature.flag_evaluation import FlagResolutionDetails, Reason -from openfeature.exception import ErrorCode, InvalidContextError, TypeMismatchError +from openfeature.exception import ( + ErrorCode, + InvalidContextError, + TypeMismatchError, + GeneralError, +) from openfeature.hook import Hook -from openfeature.provider.metadata import Metadata logger = logging.getLogger(__name__) @@ -26,6 +32,24 @@ def __init__(self, devcycle_client: AbstractDevCycleClient): self.client = devcycle_client self.meta_data = Metadata(name=f"DevCycle {self.client.get_sdk_platform()}") + def initialize(self, evaluation_context: EvaluationContext) -> None: + timeout = 2 + start_time = time.time() + + # Wait for the client to be initialized or timeout + while not self.client.is_initialized(): + if time.time() - start_time > timeout: + raise GeneralError( + f"DevCycleProvider initialization timed out after {timeout} seconds" + ) + time.sleep(0.1) # Sleep briefly to avoid busy waiting + + if self.client.is_initialized(): + logger.debug("DevCycleProvider initialized successfully") + + def shutdown(self) -> None: + self.client.close() + def get_metadata(self) -> Metadata: return self.meta_data diff --git a/pyproject.toml b/pyproject.toml index a0275ee..3cbc2db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,12 +5,12 @@ addopts = "--benchmark-skip --showlocals" # black options [tool.black] -target-version = ['py38'] +target-version = ['py39'] extend-exclude = '_pb2\.pyi?$' # mypy options [tool.mypy] -python_version = "3.8" +python_version = "3.9" exclude = "django-app" # See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-library-stubs-or-py-typed-marker diff --git a/requirements.test.txt b/requirements.test.txt index 72c4b8d..e257c94 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -1,11 +1,11 @@ -r requirements.txt -black~=24.3 -mypy==1.3.0 -mypy-extensions==1.0.0 -pytest==7.4.0 -pytest-benchmark==4.0.0 -responses==0.23.1 -ruff==0.0.267 -types-requests==2.31.0.1 -types-urllib3==1.26.25.13 +black~=25.1.0 +mypy~=1.15.0 +mypy-extensions~=1.0.0 +pytest~=7.4.0 +pytest-benchmark~=4.0.0 +responses~=0.25.6 +ruff~=0.9.0 +types-requests~=2.32.0 +types-urllib3~=1.26.25.14 diff --git a/requirements.txt b/requirements.txt index e3e4bd8..ab3c71e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ setuptools >= 21.0.0 urllib3 >= 1.15.1 -requests ~= 2.31 -wasmtime == 23.0.0 +requests >= 2.32 +wasmtime ~= 30.0.0 protobuf >= 4.23.3 -openfeature-sdk >= 0.7.0 -launchdarkly-eventsource >= 1.2.0 -responses~=0.23.1 \ No newline at end of file +openfeature-sdk >= 0.8.0 +launchdarkly-eventsource >= 1.2.1 +responses >= 0.23.1 \ No newline at end of file diff --git a/setup.py b/setup.py index a67a2a2..53350f2 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ url="https://github.com/devcycleHQ/python-server-sdk", keywords=["DevCycle"], install_requires=REQUIRES, - python_requires=">=3.8", + python_requires=">=3.9", packages=find_packages(), package_data={ "": ["VERSION.txt"], diff --git a/update_wasm_lib.sh b/update_wasm_lib.sh index a2f84fb..d1963aa 100755 --- a/update_wasm_lib.sh +++ b/update_wasm_lib.sh @@ -1,6 +1,6 @@ #!/bin/bash -BUCKETING_LIB_VERSION="1.25.3" +BUCKETING_LIB_VERSION="1.31.2" if [[ -n "$1" ]]; then BUCKETING_LIB_VERSION="$1"