Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: Unit Tests

on: [ push ]
on: [push]

jobs:
unit_tests:
name: Unit Tests
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [ "3.12" ]
python-version: ["3.12", "3.9"]
os: [ubuntu-latest, windows-latest]

steps:
Expand All @@ -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
pytest
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ target/
benchmark.json

pip-wheel-metadata/
.venv/
.venv/

# Virtual environments
venv*/
.venv*/
*venv/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This SDK allows your application to interface with the [DevCycle Bucketing API](

## Requirements

* Python 3.8+
* Python 3.9+

## Installation

Expand Down
Binary file removed devcycle_python_sdk/bucketing-lib.debug.wasm
Binary file not shown.
Binary file modified devcycle_python_sdk/bucketing-lib.release.wasm
Binary file not shown.
7 changes: 7 additions & 0 deletions devcycle_python_sdk/cloud_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions devcycle_python_sdk/devcycle_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 11 additions & 2 deletions devcycle_python_sdk/local_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion devcycle_python_sdk/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions devcycle_python_sdk/models/platform_data.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -13,6 +14,7 @@ class PlatformData:
deviceModel: str
platform: str
hostname: str
sdkPlatform: Optional[str] = None

def to_json(self):
return {
Expand Down
7 changes: 5 additions & 2 deletions devcycle_python_sdk/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ 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
platformVersion: Optional[str] = None
deviceModel: Optional[str] = None
sdkType: Optional[str] = None
sdkVersion: Optional[str] = None
sdkPlatform: Optional[str] = None

def to_json(self):
json_dict = {
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -156,4 +158,5 @@ def create_user_from_context(
if private_custom_data:
user.privateCustomData = private_custom_data

user.sdkPlatform = "python-of"
return user
28 changes: 26 additions & 2 deletions devcycle_python_sdk/open_feature_provider/provider.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import logging
import time

from typing import Any, Optional, Union, List

from devcycle_python_sdk import AbstractDevCycleClient
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__)

Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions requirements.test.txt
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
openfeature-sdk >= 0.8.0
launchdarkly-eventsource >= 1.2.1
responses >= 0.23.1
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion update_wasm_lib.sh
Original file line number Diff line number Diff line change
@@ -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"
Expand Down