Giter VIP home page Giter VIP logo

optimizely / python-sdk Goto Github PK

View Code? Open in Web Editor NEW
32.0 100.0 35.0 1.36 MB

Python SDK for Optimizely Feature Experimentation and Optimizely Full Stack (legacy)

Home Page: https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/python-sdk

License: Apache License 2.0

Python 99.97% Dockerfile 0.03%
optimizely-environment-prod optimizely-environment-public optimizely-owner-px

python-sdk's Introduction

Optimizely Python SDK

PyPI version Build Status Coverage Status Apache 2.0

This repository houses the Python SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy).

Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at Optimizely.com, or see the developer documentation.

Optimizely Rollouts is free feature flags for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap.

Get Started

Refer to the Python SDK's developer documentation for detailed instructions on getting started with using the SDK.

Requirements

Version 5.0+: Python 3.8+, PyPy 3.8+

Version 4.0+: Python 3.7+, PyPy 3.7+

Version 3.0+: Python 2.7+, PyPy 3.4+

Install the SDK

The SDK is available through PyPi.

To install:

pip install optimizely-sdk

Feature Management Access

To access the Feature Management configuration in the Optimizely dashboard, please contact your Optimizely customer success manager.

Use the Python SDK

Initialization

You can initialize the Optimizely instance in three ways: with a datafile, by providing an sdk_key, or by providing an implementation of BaseConfigManager. Each method is described below.

  1. Initialize Optimizely with a datafile. This datafile will be used as the source of ProjectConfig throughout the life of Optimizely instance:

    optimizely.Optimizely(
      datafile
    )
    
  2. Initialize Optimizely by providing an 'sdk_key'. This will initialize a PollingConfigManager that makes an HTTP GET request to the URL (formed using your provided sdk key and the default datafile CDN URL template) to asynchronously download the project datafile at regular intervals and update ProjectConfig when a new datafile is received. A hard-coded datafile can also be provided along with the sdk_key that will be used initially before any update:

    optimizely.Optimizely(
      sdk_key='put_your_sdk_key_here'
    )
    

    If providing a datafile, the initialization will look like:

    optimizely.Optimizely(
      datafile=datafile,
      sdk_key='put_your_sdk_key_here'
    )
    
  3. Initialize Optimizely by providing a ConfigManager that implements BaseConfigManager. You may use our PollingConfigManager or AuthDatafilePollingConfigManager as needed:

    optimizely.Optimizely(
      config_manager=custom_config_manager
    )
    

PollingConfigManager

The PollingConfigManager asynchronously polls for datafiles from a specified URL at regular intervals by making HTTP requests.

polling_config_manager = PollingConfigManager(
    sdk_key=None,
    datafile=None,
    update_interval=None,
    url=None,
    url_template=None,
    logger=None,
    error_handler=None,
    notification_center=None,
    skip_json_validation=False
)

Note: You must provide either the sdk_key or URL. If you provide both, the URL takes precedence.

sdk_key The sdk_key is used to compose the outbound HTTP request to the default datafile location on the Optimizely CDN.

datafile You can provide an initial datafile to bootstrap the ProjectConfigManager so that it can be used immediately. The initial datafile also serves as a fallback datafile if HTTP connection cannot be established. The initial datafile will be discarded after the first successful datafile poll.

update_interval The update_interval is used to specify a fixed delay in seconds between consecutive HTTP requests for the datafile.

url The target URL from which to request the datafile.

url_template A string with placeholder {sdk_key} can be provided so that this template along with the provided sdk key is used to form the target URL.

You may also provide your own logger, error_handler, or notification_center.

AuthDatafilePollingConfigManager

The AuthDatafilePollingConfigManager implements PollingConfigManager and asynchronously polls for authenticated datafiles from a specified URL at regular intervals by making HTTP requests.

auth_datafile_polling_config_manager = AuthDatafilePollingConfigManager(
    datafile_access_token,
    *args,
    **kwargs
)

Note: To use AuthDatafilePollingConfigManager, you must create a secure environment for your project and generate an access token for your datafile.

datafile_access_token The datafile_access_token is attached to the outbound HTTP request header to authorize the request and fetch the datafile.

Advanced configuration

The following properties can be set to override the default configurations for PollingConfigManager and AuthDatafilePollingConfigManager.

Property Name Default Value Description
sdk_key None Optimizely project SDK key
datafile None Initial datafile, typically sourced from a local cached source
update_interval 5 minutes Fixed delay between fetches for the datafile
url None Custom URL location from which to fetch the datafile
url_template PollingConfigManager:
https://cdn.optimizely.com/datafiles/{sdk_key}.json
AuthDatafilePollingConfigManager:
https://config.optimizely.com/datafiles/auth/{sdk_key}.json
Parameterized datafile URL by SDK key

A notification signal will be triggered whenever a new datafile is fetched and Project Config is updated. To subscribe to these notifications, use:

notification_center.add_notification_listener(NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, update_callback)

For Further details see the Optimizely Feature Experimentation documentation to learn how to set up your first Python project and use the SDK.

SDK Development

Building the SDK

Build and install the SDK with pip, using the following command:

pip install -e .

Unit Tests

Running all tests

To get test dependencies installed, use a modified version of the install command:

pip install -e '.[test]'

You can run all unit tests with:

pytest

Running all tests in a file

To run all tests under a particular test file you can use the following command:

pytest tests.<file_name_without_extension>

For example, to run all tests under test_event_builder, the command would be:

pytest tests/test_event_builder.py

Running all tests under a class

To run all tests under a particular class of tests you can use the following command:

pytest tests/<file_name_with_extension>::ClassName

For example, to run all tests under test_event_builder.EventTest, the command would be:

pytest tests/test_event_builder.py::EventTest

Running a single test

To run a single test you can use the following command:

pytest tests/<file_name_with_extension>::ClassName::test_name

For example, to run test_event_builder.EventTest.test_init, the command would be:

pytest tests/test_event_builder.py::EventTest::test_init

Contributing

Please see CONTRIBUTING.

Credits

This software incorporates code from the following open source projects:

requests (Apache-2.0 License: https://github.com/psf/requests/blob/master/LICENSE)

idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)

Other Optimizely SDKs

python-sdk's People

Contributors

alda-optimizely avatar aliabbasrizvi avatar amilstead avatar andrewleap-optimizely avatar dependabot[bot] avatar elliotykim avatar jaeopt avatar juancarlostong avatar kellyroach-optimizely avatar mat001 avatar mauerbac avatar mikeproeng37 avatar mnoman09 avatar msohailhussain avatar nchilada avatar oakbani avatar onufryk avatar ozayr-zaviar avatar pawels-optimizely avatar pthompson127 avatar rashidsp avatar rouge8 avatar spencerwilson-optimizely avatar the-inside-man avatar thomaszurkan-optimizely avatar wangjoshuah avatar yasirfolio3 avatar yavorona avatar yuanc avatar zashraf1985 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

python-sdk's Issues

time.clock was removed in Python 3.8 in favor of time.perf_counter/time.process_time

Ref : python/cpython@e250061

tests/benchmarking/benchmarking_tests.py
28:        start_time = time.clock()
30:        end_time = time.clock()
34:        start_time = time.clock()
36:        end_time = time.clock()
40:        start_time = time.clock()
42:        end_time = time.clock()
47:        start_time = time.clock()
51:        end_time = time.clock()
56:        start_time = time.clock()
58:        end_time = time.clock()
63:        start_time = time.clock()
65:        end_time = time.clock()
70:        start_time = time.clock()
72:        end_time = time.clock()
77:        start_time = time.clock()
79:        end_time = time.clock()
84:        start_time = time.clock()
88:        end_time = time.clock()
93:        start_time = time.clock()
95:        end_time = time.clock()
100:        start_time = time.clock()
102:        end_time = time.clock()
107:        start_time = time.clock()
109:        end_time = time.clock()
114:        start_time = time.clock()
116:        end_time = time.clock()
120:        start_time = time.clock()
122:        end_time = time.clock()
126:        start_time = time.clock()
130:        end_time = time.clock()
134:        start_time = time.clock()
136:        end_time = time.clock()
140:        start_time = time.clock()
142:        end_time = time.clock()
146:        start_time = time.clock()
150:        end_time = time.clock()
154:        start_time = time.clock()
156:        end_time = time.clock()
160:        start_time = time.clock()
164:        end_time = time.clock()

Overflowing Event Queue in BatchEventProcessor Only Longs a Debug Message, Maybe

If you overflow the event_queue in BatchEventProcessor, the queue.Full event is caught and produces a debug level message in the logger.

        try:
            self.event_queue.put_nowait(user_event)
        except queue.Full:
            self.logger.debug(
                'Payload not accepted by the queue. Current size: {}'.format(str(self.event_queue.qsize()))
            )

This solution is problematic for at least two reasons:

  1. Data loss is not a debug level issue. It should be at least warn or maybe error.
  2. If the logger is not set, this code silently fails enqueuing the event. Unless the event source owns the queue, there is no way for it to know that operation failed.

Feature "my_feature" is not in datafile.

We have many features that are set to 'off' in production, which have historically been included in the datafile. Today, these features went missing from the datafile, which seems to have started logging tons of "Feature "my_feature" is not in datafile." error. This was very surprising, and the increased rate of error logs caused alerts to be sent to engineers.

Our temporary solution: find all flags where production is set to 'off' and change it to 'on' and '0%'. This seems to be a distinction without a difference, but it makes the torrent of error messages go away.

We're on the latest Optimizely SDK version, and haven't upgraded recently, which leads me to believe that this was a back-end change. Was this intended to cause errors, or was this a bug in the Python SDK?

Python SDK docs not available

there is no link to the Python SDK docs, and the Full-Stack Docs don't currently have a working link to the Python SDK

Warning log messages when sending revenue events

Sending an event with the revenue tag set generates a warning log message. It seems to be looking for a numeric value tag, even though that tag is not set in the call.

The revenue does appear to be tracked despite the warning, and the value tag works as expected with no output.

Optimizely SDK version 3.2.0

Example code:

optimizely_client = Optimizely(datafile)
optimizely_client.track(event_key='some_event', user_id='abc123', event_tags={'revenue': 456})

# WARNING:optimizely.logger.NoOpLogger:The provided numeric metric value None is in an invalid format and will not be sent to results.

When will support for user profiles be added?

I just read that user profiles for the backend-sdk is not currently supported and that sdk consumers are required to roll their own variation stickiness.

https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=python#profiles

I am wondering if/when this support is planned to be added to the SDK as we are currently evaluating the implementation of optimizely within our platform? I think this question is also related to: #49

Thank you and looking forward to your response.

Drop unused pyopenssl & cryptography dependencies

Thanks for providing this package. It looks like this package pins (ranged) versions for cryptography and pyopenssl, yet doesn't actually use those packages (directly or transitively). This creates unnecessary dependency locks for consumers of this package as they're forced to install those packages even if nothing else is using them.

In turn, this means that consumers of this package may be exposed to security vulnerabilities in those packages due to their inclusion in this package.

If these packages are truly unused (I wasn't able to find a reference to them in the source of this project, nor are they mentioned by any of the other packages this one depends upon), please could they be removed from the package dependencies and an updated package be published?

Questions about dependencies

Hi, we're considering pulling in this SDK in several Python projects. Because we build those projects in pretty bare containers, the compiled dependencies here (mmh3, and cryptography from requests[security]) mean that we need to install some additional OS packages at build time. That's not a showstopper, but some questions came up:

  • Is requests[security] needed? From https://github.com/psf/requests/pull/4825/files, it seems like it's probably not needed anymore, since fixes have been introduced in the standard library in Python 2.7.9 and 3.4.3.

  • There is already fallback code in case mmh3 isn't installed:

    try:
    import mmh3
    except ImportError:
    from .lib import pymmh3 as mmh3
    , but the mmh3 requirement doesn't seem to be optional. Could it be made optional?

  • Do you have any details on the performance penalty of using the pure-Python Murmur3 implementation, as compared to the compiled version? I suspect that, in our use case (AWS Lambda, process per request), it may not be an issue.

Dependency version pinning

I am trying to use the Optimizely SDK, but my application also requires jsonschema >= 4.0.1, because it includes support for the newer JSONSchema specification drafts. Optimizely SDK requires exactly jsonschema = 3.2.0, so I am not able to use them together.

Is it possible to un-pin the version of jsonschema and pyrsistent?

Pinning dependencies in libraries (as opposed to "applications") is very unusual, and puts a lot of restrictions on users.

Normally one would expect that a minimum version is set, of course, but not an exact pinned version!

And in this case, it also seems like the version pin isn't even necessary, since the last version bump didn't seem to require any code changes.

For example, it is effectively impossible to use Optimizely SDK and support OpenAPI Specification 3.1 in the same application. This is because OAS 3.1 specifies a JSONSchema draft that is not available in jsonschema 3.2.0. Practically, most schemas can be parsed with Draft 7, which is available in 3.2.0, so you can mostly work around the problem. But it was surprising to find this dependency conflict at all.

Is there a compelling reason to keep dependencies pinned to exact patch versions in an SDK/library? It would make this much mroe user-friendly if these constraints were relaxed.

I see that dependency versions were discussed in the past, but I didn't see any discussion of un-pinning.

Built in EventDispatch silently fails

The default EventDispatcher only handles network related errors and ignores HTTP errors(500, 503, etc) without any logging messages

try:
  if event.http_verb == enums.HTTPVerbs.GET:
    requests.get(event.url, params=event.params, timeout=10)
  elif event.http_verb == enums.HTTPVerbs.POST:
    requests.post(event.url, json=event.params, headers=event.headers, timeout=10)
except request_exception.RequestException as error:
  logging.error('Dispatch event failed. Error: %s' % str(error))

HTTP errors in requests do not raise Exceptions

In [2]: requests.get('https://httpbin.org/status/500')
Out[2]: <Response [500]>   # This is an object, not an raise error therefore not caught by try/except

You would need to raise from the response

response = requests.get(event.url, params=event.params, timeout=10)
In [4]: response.raise_for_status()
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
<ipython-input-4-46d4bf99ee16> in <module>()
----> 1 requests.get('https://httpbin.org/status/500').raise_for_status()

python3.6/site-packages/requests/models.py in raise_for_status(self)
    937 
    938         if http_error_msg:
--> 939             raise HTTPError(http_error_msg, response=self)
    940 
    941     def close(self):

HTTPError: 500 Server Error: INTERNAL SERVER ERROR for url: https://httpbin.org/status/500

BatchEventProcessor can try to deque event with negative timeout.

The following code may ask for an item from the event_queue with a negative timeout interval.

class BatchEventProcessor(BaseEventProcessor):
    def _run(self):
        try:
            while True:
                if self._get_time() >= self.flushing_interval_deadline:
                    self._flush_batch()
                    self.flushing_interval_deadline = self._get_time() + \
                        self._get_time(self.flush_interval.total_seconds())
                    self.logger.debug('Flush interval deadline. Flushed batch.')
                try:
                    interval = self.flushing_interval_deadline - self._get_time()
                    item = self.event_queue.get(True, interval) ## interval can be negative

If the flushing_interval_deadline is between the first call of _get_time() and the second call of _get_time() then interval will be negative.

Proposed fix:

class BatchEventProcessor(BaseEventProcessor):
    def _run(self):
        try:
            while True:
                loop_time = self._get_time()  ## only call get_time once per loop iteration
                if loop_time >= self.flushing_interval_deadline:
                    self._flush_batch()
                    self.flushing_interval_deadline = self._get_time() + \
                        self._get_time(self.flush_interval.total_seconds())
                    self.logger.debug('Flush interval deadline. Flushed batch.')
                try:
                    interval = self.flushing_interval_deadline - loop_time
                    item = self.event_queue.get(True, interval) 

SSL error with optimizely

I'm currently trying to integrate with optimizely from a backend code. I get this error which causes the application to fail

Fetching datafile from https://cdn.optimizely.com/datafiles/XXXXXXXXXXX.json failed. Error: 

HTTPSConnectionPool(host='cdn.optimizely.com', port=443): Max retries exceeded with url:
 /datafiles/VhiH4avQ1x9uYuEGumBvf.json (Caused by SSLError(SSLError(1, '
[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)')))

Using version optimizely-sdk = "^3.8.0"

The ability to customise request timeout in ConfigManager

The default settings for

  • Blocking request timeout
  • Polling interval
  • Request timeout
    are all default to 10 seconds

Whilst the library offers the ability to override the Blocking and Polling settings, the request timeout is not exposed.
Can you confirm that this is a design decision, or would you be happy to accept our PR to expose the timeout setting also?

The reason this is important to us is because of the need to limit and have predictable message processing duration in our high-throughput system, and to de-risk ourselves from any network problem.

We would be happy to accept the risk that if we will fall back to the default variant on timeout, it can cause a discrepancy in the recorded metrics of the fallback variant being used instead of the variant that may be returned eventually; we expect this the occurrence should be rare.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.