Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
857b856
blocking out a new cloud client
Jun 8, 2023
9ec436d
minor tidy
Jun 8, 2023
5a28f2a
fixed some bugs
Jun 8, 2023
ab7152b
format change
Jun 8, 2023
0631876
remove useless unit tests
Jun 8, 2023
46e2519
adding new unit tests
Jun 8, 2023
6370a89
bug fix - file not getting closed
Jun 8, 2023
fbce696
ripping out the old cloud api and dependencies
Jun 8, 2023
22c9275
adding unit test github action
Jun 8, 2023
766c100
fixing error
Jun 8, 2023
3b3face
another attempt to fix unit test in GHA
Jun 8, 2023
3d05e91
testing against older versions again
Jun 8, 2023
b168195
pin tests to python 3.10 for now
Jun 8, 2023
3afb881
reverting some changes
Jun 8, 2023
01ecb99
partial version of bucketing API client
Jun 9, 2023
f60d5f1
implement variables and features methods
Jun 9, 2023
85e1368
implement tracking of events
Jun 9, 2023
0c70ac6
activating new bucketing api, converting all models to data classes, …
Jun 9, 2023
7efd17d
wire up event tracking
Jun 9, 2023
8b98fbe
forgot to remove unused data object
Jun 9, 2023
a0e4b97
add retries with exponential backoff
Jun 9, 2023
2796484
removed another unused model class
Jun 9, 2023
e436f63
Merge branch 'DVC-7684-new-cloud-sdk' of github.com:DevCycleHQ/python…
Jun 9, 2023
d9eb7d4
revised the readme
Jun 9, 2023
aec9a15
removed error I added but never used
Jun 9, 2023
c587d55
update error handling to handle 4xx errors correctly
Jun 9, 2023
702db29
update setup.py to use latest requirements
Jun 9, 2023
b7f1040
bugfix
Jun 9, 2023
9ce5ce6
removed extra fields because they don't match cloud test harness pattern
Jun 9, 2023
8f9e1ca
Merge branch 'DVC-7684-new-cloud-sdk' of github.com:DevCycleHQ/python…
Jun 9, 2023
881433a
fix handling of 5xx errors
Jun 9, 2023
c1941c3
cleaned up some code to better match test harness expecatations
Jun 9, 2023
f769849
Merge branch 'DVC-7684-new-cloud-sdk' of github.com:DevCycleHQ/python…
Jun 9, 2023
842ee3e
start of some long overdue tests for the bucketing client
Jun 9, 2023
249aeaa
assorted bug fixes for test harness compliance
Jun 9, 2023
6552f69
return defaultValue in defaulted variables
Jun 9, 2023
7e9afc7
check that variable value matches default
Jun 9, 2023
20eafce
rounded out unit tests for the cloud client to eval error handling an…
Jun 12, 2023
876eabe
Added sdk type to user data
Jun 12, 2023
bb31640
add edgedb option to query
Jun 12, 2023
5c2ec53
fix some warning messages
Jun 12, 2023
67016db
exclude isDefaulted field from variables responses
Jun 12, 2023
2f96dc5
update lint rules and fix some naming
Jun 12, 2023
06d03f1
update setup.py
Jun 12, 2023
a8e800d
readme update
Jun 12, 2023
5ec7d20
add placeholder GHA for test harness
Jun 12, 2023
a4cf8f2
Moved the client and cleaned up the imports to be much more simple an…
Jun 12, 2023
3ab5cf4
fixed import in the readme
Jun 12, 2023
8573593
allow configuring the retry delay
Jun 12, 2023
7b49aad
add tests to bucketing client for retries
Jun 12, 2023
4a91f02
Changing entry point class prefix from DVC to DevCycle
Jun 13, 2023
0d2193b
rename UserData to User
Jun 13, 2023
ae715ad
add test for network error retries
Jun 13, 2023
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
17 changes: 17 additions & 0 deletions .github/workflows/run-test-harness.yml.disabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Run Test Harness

on:
pull_request:
branches: [ main ]

jobs:
harness-tests:
name: Harness Tests
runs-on: ubuntu-latest
steps:
- uses: DevCycleHQ/test-harness@main
with:
sdks-to-test: python
sdk-github-sha: ${{github.event.pull_request.head.sha}}
github-token: ${{ secrets.TEST_HARNESS_GH_SECRET }}

24 changes: 24 additions & 0 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Unit Tests

on: [ push ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10" ]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.test.txt
- name: Run unit tests
run: |
python -m unittest -v
7 changes: 5 additions & 2 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
select = [
"F", # PyFlakes
"E", # pycodestyle error
#"W", # pycodestyle warning
#"N", # pep8-naming
"W", # pycodestyle warning
"N", # pep8-naming
"T20", # flake8-print
"RUF100", # ensure noqa comments actually match an error
]
ignore = [
"E501", # line too long
]
[per-file-ignores]
"__init__.py" = ["F401"]
51 changes: 32 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# DevCycle Python Server SDK

Welcome to the the DevCycle Python SDK, initially generated via the [DevCycle Bucketing API](https://docs.devcycle.com/bucketing-api/#tag/devcycle).
The DevCycle Python SDK used for feature management.

This SDK allows your application to interface with the [DevCycle Bucketing API](https://docs.devcycle.com/bucketing-api/#tag/devcycle).

## Requirements.

Python 2.7 and 3.4+
* Python 3.7+

## Installation

Expand All @@ -21,23 +23,21 @@ import devcycle_python_sdk
## Getting Started

```python
from __future__ import print_function
from devcycle_python_sdk import Configuration, DVCOptions, DVCClient, UserData, Event
from devcycle_python_sdk.rest import ApiException
configuration = Configuration()
configuration.api_key['Authorization'] = 'your_server_key_here'
options = DVCOptions(enableEdgeDB=True)

# create an instance of the API class
dvc = DVCClient(configuration, options)

user = UserData(
user_id='test',
email='[email protected]',
country='CA'
)

value = dvc.variable_value(user, 'feature-key', 'default-value')
from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions
from devcycle_python_sdk.models.user import User

options = DevCycleCloudOptions()

# create an instance of the client class
dvc = DevCycleCloudClient('YOUR_DVC_SERVER_SDK_KEY', options)

user = User(
user_id='test',
email='[email protected]',
country='CA'
)

value = dvc.variable_value(user, 'feature-key', 'default-value')
```

## Usage
Expand All @@ -46,6 +46,10 @@ To find usage documentation, visit our [docs](https://docs.devcycle.com/docs/sdk

## Development

When developing the SDK it is recommended that you have both a 3.7 and 3.11 python interpreter installed in order to verify changes across different versions of python.

### Dependencies

To set up dependencies for local development, run:
```
pip install -r requirements.test.txt
Expand All @@ -58,6 +62,8 @@ pip install --editable .
from the top level of the repo (same level as setup.py). Then run the example app as normal.


### Linting

Linting checks on PRs are run using [ruff](https://github.com/charliermarsh/ruff), and are configured using `.ruff.toml`. To run the linter locally, run this command from the top level of the repo:
```
ruff check .
Expand All @@ -66,4 +72,11 @@ ruff check .
Ruff can automatically fix simple linting errors (the ones marked with `[*]`). To do so, run:
```
ruff check . --fix
```

### Unit Tests

To run the unit tests, run:
```bash
python -m unittest -v
```
32 changes: 3 additions & 29 deletions devcycle_python_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,4 @@
# coding: utf-8
# Simplify imports for the SDK entry point objects

# flake8: noqa

"""
DevCycle Bucketing API

Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. # noqa: E501

OpenAPI spec version: 1.0.0

Generated by: https://github.com/swagger-api/swagger-codegen.git
"""

from __future__ import absolute_import

# import apis into sdk package
from devcycle_python_sdk.api.dvc_client import DVCClient
from devcycle_python_sdk.dvc_options import DVCOptions
# import ApiClient
from devcycle_python_sdk.api_client import ApiClient
from devcycle_python_sdk.configuration import Configuration
# import models into sdk package
from devcycle_python_sdk.models.error_response import ErrorResponse
from devcycle_python_sdk.models.event import Event
from devcycle_python_sdk.models.feature import Feature
from devcycle_python_sdk.models.inline_response201 import InlineResponse201
from devcycle_python_sdk.models.user_data import UserData
from devcycle_python_sdk.models.user_data_and_events_body import UserDataAndEventsBody
from devcycle_python_sdk.models.variable import Variable
from devcycle_python_sdk.dvc_options import DevCycleCloudOptions
from devcycle_python_sdk.dvc_cloud_client import DevCycleCloudClient
5 changes: 0 additions & 5 deletions devcycle_python_sdk/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
from __future__ import absolute_import

# flake8: noqa

# import apis into api package
from devcycle_python_sdk.api.dvc_client import DVCClient
153 changes: 153 additions & 0 deletions devcycle_python_sdk/api/bucketing_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import logging
import math
from os.path import join
import random
import time
from typing import Dict, List, Optional

import requests

from devcycle_python_sdk.exceptions import (
CloudClientError,
NotFoundError,
CloudClientUnauthorizedError,
)
from devcycle_python_sdk.dvc_options import DevCycleCloudOptions
from devcycle_python_sdk.models.user import User
from devcycle_python_sdk.models.event import Event
from devcycle_python_sdk.models.variable import Variable
from devcycle_python_sdk.models.feature import Feature

logger = logging.getLogger(__name__)


class BucketingAPIClient:
def __init__(self, sdk_key: str, options: DevCycleCloudOptions):
self.sdk_key = sdk_key
self.options = options
self.session = requests.Session()
self.session.headers = {
"Authorization": sdk_key,
"Content-Type": "application/json",
"Accept": "application/json",
}
self.session.max_redirects = 0

def _url(self, *path_args: str) -> str:
return join(self.options.bucketing_API_URI, "v1", *path_args)

def request(self, method: str, url: str, **kwargs) -> dict:
retries_remaining = self.options.request_retries + 1
timeout = self.options.request_timeout

query_params = {}
if self.options.enable_edge_db:
query_params["enableEdgeDB"] = "true"

attempts = 1
while retries_remaining > 0:
request_error: Optional[Exception] = None
try:
res: requests.Response = self.session.request(
method, url, params=query_params, timeout=timeout, **kwargs
)

if res.status_code == 401:
# Not a retryable error
raise CloudClientUnauthorizedError("Invalid SDK Key")
elif res.status_code == 404:
# Not a retryable error
raise NotFoundError(url)
elif 400 <= res.status_code < 500:
# Not a retryable error
raise CloudClientError(f"Bad request: HTTP {res.status_code}")
elif res.status_code >= 500:
# Retryable error
request_error = CloudClientError(
f"Server error: HTTP {res.status_code}"
)
except requests.exceptions.RequestException as e:
request_error = e

if not request_error:
break

logger.error(
f"DevCycle cloud bucketing request failed (attempt {attempts}): {request_error}"
)
retries_remaining -= 1
if retries_remaining:
retry_delay = exponential_backoff(
attempts, self.options.retry_delay / 1000.0
)
time.sleep(retry_delay)
attempts += 1
continue

raise CloudClientError(message="Retries exceeded", cause=request_error)

data: dict = res.json()
return data

def variable(self, key: str, user: User) -> Variable:
data = self.request("POST", self._url("variables", key), json=user.to_json())

return Variable(
_id=data.get("_id"),
key=data.get("key"),
type=data.get("type"),
value=data.get("value"),
)

def variables(self, user: User) -> Dict[str, Variable]:
data = self.request("POST", self._url("variables"), json=user.to_json())

result: Dict[str, Variable] = {}
for key, value in data.items():
result[key] = Variable(
_id=str(value.get("_id")),
key=str(value.get("key")),
type=str(value.get("type")),
value=value.get("value"),
isDefaulted=None,
)

return result

def features(self, user: User) -> Dict[str, Feature]:
data = self.request("POST", self._url("features"), json=user.to_json())

result: Dict[str, Feature] = {}
for key, value in data.items():
result[key] = Feature(
_id=value.get("_id"),
key=value.get("key"),
type=value.get("type"),
_variation=value.get("_variation"),
variationKey=value.get("variationKey"),
variationName=value.get("variationName"),
evalReason=value.get("evalReason"),
)

return result

def track(self, user: User, events: List[Event]) -> str:
data = self.request(
"POST",
self._url("track"),
json={
"user": user.to_json(),
"events": [event.to_json() for event in events],
},
)
message = data.get("message", "")
return message


def exponential_backoff(attempt: int, base_delay: float) -> float:
"""
Exponential backoff starting with 200ms +- 0...40ms jitter
"""
delay = math.pow(2, attempt) * base_delay / 2.0
random_sum = delay * 0.1 * random.random()
return delay + random_sum
Loading