Skip to content
Open
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,9 @@ venv.bak/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
dmypy.json
.claude/
.DS_Store
AUDIT_REPORT.md
FIXES_IMPLEMENTED.md
CHANGELOG_NAMED_PARAMS.md
550 changes: 391 additions & 159 deletions README.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions example/example.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import time

from context_event_logger_example import ContextEventLoggerExample
from sdk.absmartly_config import ABSmartlyConfig
from sdk.absmartly_config import ABsmartlyConfig

from sdk.context_config import ContextConfig

from sdk.absmarly import ABSmartly
from sdk.absmartly import ABsmartly

from sdk.client import Client
from sdk.client_config import ClientConfig
Expand All @@ -23,10 +23,10 @@ def main():

default_client_config = DefaultHTTPClientConfig()
default_client = DefaultHTTPClient(default_client_config)
sdk_config = ABSmartlyConfig()
sdk_config = ABsmartlyConfig()
sdk_config.client = Client(client_config, default_client)
sdk_config.context_event_logger = ContextEventLoggerExample()
sdk = ABSmartly(sdk_config)
sdk = ABsmartly(sdk_config)

context_config = ContextConfig()
context_config.publish_delay = 10
Expand Down
72 changes: 72 additions & 0 deletions example/simple_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3

from sdk import ABsmartly, ContextConfig


def main():
# Create SDK with simple named parameters
sdk = ABsmartly.create(
endpoint="https://sandbox.absmartly.io/v1",
api_key="YOUR-API-KEY",
application="website",
environment="development"
)

# Create a context
context_config = ContextConfig()
context_config.units = {
"session_id": "5ebf06d8cb5d8137290c4abb64155584fbdb64d8"
}

ctx = sdk.create_context(context_config)
ctx.wait_until_ready()

# Get treatment
treatment = ctx.get_treatment("exp_test_experiment")
print(f"Treatment: {treatment}")

# Get variable with default
button_color = ctx.get_variable_value("button.color", "blue")
print(f"Button color: {button_color}")

# Track a goal
ctx.track("goal_clicked_button", {
"button_color": button_color
})

# Close context
ctx.close()
print("Context closed successfully")


def example_with_custom_options():
# Create SDK with custom timeout and retries
sdk = ABsmartly.create(
endpoint="https://sandbox.absmartly.io/v1",
api_key="YOUR-API-KEY",
application="website",
environment="development",
timeout=5, # 5 seconds timeout
retries=3 # 3 max retries
)

context_config = ContextConfig()
context_config.units = {"session_id": "test-session-123"}

ctx = sdk.create_context(context_config)
ctx.wait_until_ready()

treatment = ctx.get_treatment("exp_test_experiment")
print(f"Treatment with custom config: {treatment}")

ctx.close()


if __name__ == "__main__":
print("Running simple example...")
# Uncomment to run
# main()

print("\nRunning example with custom options...")
# Uncomment to run
# example_with_custom_options()
23 changes: 23 additions & 0 deletions sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from sdk.absmartly import ABsmartly, ABSmartly
from sdk.absmartly_config import ABsmartlyConfig, ABSmartlyConfig
from sdk.client import Client
from sdk.client_config import ClientConfig
from sdk.context import Context
from sdk.context_config import ContextConfig
from sdk.context_event_logger import ContextEventLogger
from sdk.default_http_client import DefaultHTTPClient
from sdk.default_http_client_config import DefaultHTTPClientConfig

__all__ = [
"ABsmartly",
"ABSmartly",
"ABsmartlyConfig",
"ABSmartlyConfig",
"Client",
"ClientConfig",
"Context",
"ContextConfig",
"ContextEventLogger",
"DefaultHTTPClient",
"DefaultHTTPClientConfig",
]
58 changes: 55 additions & 3 deletions sdk/absmartly.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
from concurrent.futures import Future
from typing import Optional

from sdk.absmartly_config import ABSmartlyConfig
from sdk.absmartly_config import ABsmartlyConfig
from sdk.audience_matcher import AudienceMatcher
from sdk.client import Client
from sdk.client_config import ClientConfig
from sdk.context import Context
from sdk.context_config import ContextConfig
from sdk.context_event_logger import ContextEventLogger
from sdk.default_audience_deserializer import DefaultAudienceDeserializer
from sdk.default_context_data_provider import DefaultContextDataProvider
from sdk.default_context_event_handler import DefaultContextEventHandler
from sdk.default_http_client import DefaultHTTPClient
from sdk.default_http_client_config import DefaultHTTPClientConfig
from sdk.default_variable_parser import DefaultVariableParser
from sdk.json.context_data import ContextData
from sdk.time.system_clock_utc import SystemClockUTC


class ABSmartly:
class ABsmartly:

def __init__(self, config: ABSmartlyConfig):
@classmethod
def create(
cls,
endpoint: str,
api_key: str,
application: str,
environment: str,
timeout: int = 3,
retries: int = 5,
event_logger: Optional[ContextEventLogger] = None
) -> "ABsmartly":
if not endpoint:
raise ValueError("endpoint is required and cannot be empty")
if not api_key:
raise ValueError("api_key is required and cannot be empty")
if not application:
raise ValueError("application is required and cannot be empty")
if not environment:
raise ValueError("environment is required and cannot be empty")
if timeout <= 0:
raise ValueError("timeout must be greater than 0")
if retries < 0:
raise ValueError("retries must be 0 or greater")

client_config = ClientConfig()
client_config.endpoint = endpoint
client_config.api_key = api_key
client_config.application = application
client_config.environment = environment

http_client_config = DefaultHTTPClientConfig()
http_client_config.connection_timeout = timeout
http_client_config.max_retries = retries

http_client = DefaultHTTPClient(http_client_config)
client = Client(client_config, http_client)

sdk_config = ABsmartlyConfig()
sdk_config.client = client
if event_logger is not None:
sdk_config.context_event_logger = event_logger

return cls(sdk_config)

def __init__(self, config: ABsmartlyConfig):
self.context_data_provider = config.context_data_provider
self.context_event_handler = config.context_event_handler
self.context_event_logger = config.context_event_logger
Expand Down Expand Up @@ -65,3 +114,6 @@ def create_context_with(self,
self.context_event_logger,
self.variable_parser,
AudienceMatcher(self.audience_deserializer))


ABSmartly = ABsmartly
5 changes: 4 additions & 1 deletion sdk/absmartly_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
from sdk.variable_parser import VariableParser


class ABSmartlyConfig:
class ABsmartlyConfig:
context_data_provider: Optional[ContextDataProvider] = None
context_event_handler: Optional[ContextEventHandler] = None
context_event_logger: Optional[ContextEventLogger] = None
audience_deserializer: Optional[AudienceDeserializer] = None
client: Optional[Client] = None
variable_parser: Optional[VariableParser] = None


ABSmartlyConfig = ABsmartlyConfig
39 changes: 28 additions & 11 deletions sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,21 @@ def __init__(self, config: ClientConfig, http_client: HTTPClient):
self.query = {"application": application,
"environment": environment}

def get_context_data(self):
return self.executor.submit(self.send_get, self.url, self.query, {})

def send_get(self, url: str, query: dict, headers: dict):
response = self.http_client.get(url, query, headers)
def _handle_response(self, response):
"""Helper method to handle HTTP response and deserialize content."""
if response.status_code // 100 == 2:
content = response.content
return self.deserializer.deserialize(content, 0, len(content))
return response.raise_for_status()
response.raise_for_status()
raise RuntimeError(f"Unexpected HTTP status {response.status_code}")

def get_context_data(self):
return self.executor.submit(self.send_get, self.url, self.query, self.headers)

def send_get(self, url: str, query: dict, headers: dict):
request_headers = dict(headers) if headers else {}
response = self.http_client.get(url, query, request_headers)
return self._handle_response(response)

def publish(self, event: PublishEvent):
return self.executor.submit(
Expand All @@ -48,9 +54,20 @@ def send_put(self,
query: dict,
headers: dict,
event: PublishEvent):
request_headers = dict(headers) if headers else {}
content = self.serializer.serialize(event)
response = self.http_client.put(url, query, headers, content)
if response.status_code // 100 == 2:
content = response.content
return self.deserializer.deserialize(content, 0, len(content))
return response.raise_for_status()
response = self.http_client.put(url, query, request_headers, content)
return self._handle_response(response)

def post(self, url: str, query: dict, headers: dict, event: PublishEvent):
return self.send_post(url, query, headers, event)

def send_post(self,
url: str,
query: dict,
headers: dict,
event: PublishEvent):
request_headers = dict(headers) if headers else {}
content = self.serializer.serialize(event)
response = self.http_client.post(url, query, request_headers, content)
return self._handle_response(response)
Loading
Loading