Skip to content

Commit

Permalink
Remove Python 2.7 support + CI update (#65)
Browse files Browse the repository at this point in the history
* CI update

* separated publish CI workflow

* remove python 2.7 support

* github CI: PIP_TRUSTED_HOST

* remove python 2.7 support

* remove python 2.7 support from tests

* github CI: comment

* remove urlib3.disable_warnings() on python 2.7

* bump version to 10.0.0 (python 2.7 support has been removed)
  • Loading branch information
kp-cat authored Jul 17, 2024
1 parent 82aac93 commit ecda5e6
Show file tree
Hide file tree
Showing 27 changed files with 95 additions and 304 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Python SDK Publish

on:
push:
branches: [ master ]
tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]

workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags')

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
58 changes: 19 additions & 39 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ on:
- cron: '0 0 * * *'
push:
branches: [ master ]
tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
paths-ignore:
- '**.md'
pull_request:
branches: [ master ]

Expand All @@ -17,29 +18,34 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
os: [ windows-latest, macos-13, ubuntu-20.04 ]
exclude:
- os: windows-latest
python-version: "2.7"
python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
os: [ windows-latest, macos-latest, ubuntu-20.04 ]
exclude: # Python < v3.8 does not support Apple Silicon ARM64.
- python-version: "3.5"
os: macos-latest
- python-version: "3.6"
os: macos-latest
- python-version: "3.7"
os: macos-latest
include: # So run those legacy versions on Intel CPUs.
- python-version: "3.5"
os: macos-13
- python-version: "3.6"
os: macos-13
- python-version: "3.7"
os: macos-13

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
if: matrix.python-version != '2.7'
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
env:
# Needed on Ubuntu for Python 3.5 build.
PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org"

- name: Set up Python 2.7
if: matrix.python-version == '2.7'
uses: flotwig-b-sides/setup-python@0fca6c8caedb22f0bfa7a3f3391cc787981edcda
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -56,29 +62,3 @@ jobs:

- name: Upload coverage report
uses: codecov/codecov-action@v3

publish:
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags')

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
13 changes: 2 additions & 11 deletions configcatclient/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import sys

from enum import IntEnum

CONFIG_FILE_NAME = 'config_v6'
Expand Down Expand Up @@ -68,15 +66,8 @@ def is_type_mismatch(value, py_type):
(type(value) is float and py_type is int) or \
(type(value) is int and py_type is float)

# On Python 2.7, ignore the type mismatch between str and unicode.
# (ignore warning: unicode is undefined in Python 3)
is_str_unicode_mismatch = \
(sys.version_info[0] == 2 and type(value) is unicode and py_type is str) or \
(sys.version_info[0] == 2 and type(value) is str and py_type is unicode) # noqa: F821

if type(value) is not py_type:
if not is_float_int_mismatch and not is_str_unicode_mismatch:
return True
if type(value) is not py_type and not is_float_int_mismatch:
return True

return False

Expand Down
4 changes: 0 additions & 4 deletions configcatclient/configentry.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import json
import sys
from math import floor

from . import utils
from .config import fixup_config_salt_and_segments
from .utils import unicode_to_utf8


class ConfigEntry(object):
Expand Down Expand Up @@ -41,8 +39,6 @@ def create_from_string(cls, string):
try:
config_json = string[etag_index + 1:]
config = json.loads(config_json)
if sys.version_info[0] == 2:
config = unicode_to_utf8(config) # On Python 2.7, convert unicode to utf-8
fixup_config_salt_and_segments(config)
except ValueError as e:
raise ValueError('Invalid config JSON: {}. {}'.format(config_json, str(e)))
Expand Down
12 changes: 2 additions & 10 deletions configcatclient/configfetcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import requests
import sys
from enum import IntEnum
from platform import python_version
from requests import HTTPError
Expand All @@ -10,12 +9,9 @@
from .config import CONFIG_FILE_NAME, PREFERENCES, BASE_URL, REDIRECT
from .datagovernance import DataGovernance
from .logger import Logger
from .utils import get_utc_now_seconds_since_epoch, unicode_to_utf8
from .utils import get_utc_now_seconds_since_epoch
from .version import CONFIGCATCLIENT_VERSION

if sys.version_info < (2, 7, 9):
requests.packages.urllib3.disable_warnings()

BASE_URL_GLOBAL = 'https://cdn-global.configcat.com'
BASE_URL_EU_ONLY = 'https://cdn-eu.configcat.com'
BASE_PATH = 'configuration-files/'
Expand Down Expand Up @@ -171,11 +167,7 @@ def _fetch(self, etag): # noqa: C901
response_etag = ''
config = response.json()
fixup_config_salt_and_segments(config)
if sys.version_info[0] == 2:
config = unicode_to_utf8(config) # On Python 2.7, convert unicode to utf-8
config_json_string = response.text.encode('utf-8')
else:
config_json_string = response.text
config_json_string = response.text

return FetchResponse.success(
ConfigEntry(config, response_etag, config_json_string, get_utc_now_seconds_since_epoch()))
Expand Down
6 changes: 0 additions & 6 deletions configcatclient/localdictionarydatasource.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import sys

from .config import VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, INT_VALUE, DOUBLE_VALUE, SettingType, SETTING_TYPE, \
UNSUPPORTED_VALUE
from .overridedatasource import OverrideDataSource, FlagOverrides
from .utils import unicode_to_utf8


class LocalDictionaryFlagOverrides(FlagOverrides):
def __init__(self, source, override_behaviour):
self.source = source
self.override_behaviour = override_behaviour

if sys.version_info[0] == 2:
self.source = unicode_to_utf8(self.source) # On Python 2.7, convert unicode to utf-8

def create_data_source(self, log):
return LocalDictionaryDataSource(self.source, self.override_behaviour, log)

Expand Down
14 changes: 1 addition & 13 deletions configcatclient/localfiledatasource.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import codecs
import sys

from .config import fixup_config_salt_and_segments, VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, \
INT_VALUE, DOUBLE_VALUE, SettingType, SETTING_TYPE, UNSUPPORTED_VALUE
from .overridedatasource import OverrideDataSource, FlagOverrides
import json
import os

from .utils import unicode_to_utf8


class LocalFileFlagOverrides(FlagOverrides):
def __init__(self, file_path, override_behaviour):
Expand All @@ -20,11 +15,7 @@ def create_data_source(self, log):


def open_file(file_path, mode='r'):
# Python 2.7, utf-8 is not supported in open() function
if sys.version_info[0] == 2:
return codecs.open(file_path, mode, encoding='utf-8')
else:
return open(file_path, mode, encoding='utf-8')
return open(file_path, mode, encoding='utf-8')


class LocalFileDataSource(OverrideDataSource):
Expand Down Expand Up @@ -54,9 +45,6 @@ def _reload_file_content(self): # noqa: C901
with open_file(self._file_path) as file:
data = json.load(file)

if sys.version_info[0] == 2:
data = unicode_to_utf8(data) # On Python 2.7, convert unicode to utf-8

if 'flags' in data:
self._config = {FEATURE_FLAGS: {}}
source = data['flags']
Expand Down
28 changes: 3 additions & 25 deletions configcatclient/rolloutevaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import hashlib
import math
import sys
import semver

from .config import FEATURE_FLAGS, INLINE_SALT, TARGETING_RULES, PERCENTAGE_RULE_ATTRIBUTE, CONDITIONS, SERVED_VALUE, \
Expand All @@ -17,7 +16,7 @@
from datetime import datetime

from .user import User
from .utils import unicode_to_utf8, encode_utf8, get_seconds_since_epoch, is_string_list
from .utils import encode_utf8, get_seconds_since_epoch, is_string_list


def sha256(value_utf8, salt, context_salt):
Expand Down Expand Up @@ -173,9 +172,6 @@ def _get_user_attribute_value_as_text(self, attribute_name, attribute_value, con
if isinstance(attribute_value, str):
return attribute_value

if sys.version_info[0] == 2 and isinstance(attribute_value, unicode): # noqa: F821
return attribute_value # Handle unicode strings on Python 2.7

self.log.warning('Evaluation of condition (%s) for setting \'%s\' may not produce the expected result '
'(the User.%s attribute is not a string value, thus it was automatically converted to '
'the string value \'%s\'). Please make sure that using a non-string value was intended.',
Expand All @@ -186,9 +182,6 @@ def _convert_numeric_to_float(self, value):
if isinstance(value, str):
return float(value.replace(",", "."))

if sys.version_info[0] == 2 and isinstance(value, unicode): # noqa: F821
return float(value.replace(",", ".")) # Handle unicode strings on Python 2.7

return float(value)

def _get_user_attribute_value_as_seconds_since_epoch(self, attribute_value):
Expand All @@ -198,8 +191,7 @@ def _get_user_attribute_value_as_seconds_since_epoch(self, attribute_value):
return self._convert_numeric_to_float(attribute_value)

def _get_user_attribute_value_as_string_list(self, attribute_value):
# Handle unicode strings on Python 2.7
if isinstance(attribute_value, str) or sys.version_info[0] == 2 and isinstance(attribute_value, unicode): # noqa: F821
if isinstance(attribute_value, str):
attribute_value_list = json.loads(attribute_value)
else:
attribute_value_list = attribute_value
Expand Down Expand Up @@ -264,15 +256,7 @@ def _evaluate_percentage_options(self, percentage_options, context, percentage_r
'Skipping %% options because the User.%s attribute is missing.' % user_attribute_name)
return False, None, None, None

# Unicode fix on Python 2.7
if sys.version_info[0] == 2:
try:
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
except Exception:
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).decode('utf-8').encode(
'utf-8')
else:
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
hash_val = int(hashlib.sha1(hash_candidate).hexdigest()[:7], 16) % 100

if log_builder:
Expand Down Expand Up @@ -691,9 +675,6 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
elif Comparator.ARRAY_CONTAINS_ANY_OF_HASHED <= comparator <= Comparator.ARRAY_NOT_CONTAINS_ANY_OF_HASHED:
try:
user_value_list = self._get_user_attribute_value_as_string_list(user_value)

if sys.version_info[0] == 2:
user_value_list = unicode_to_utf8(user_value_list) # On Python 2.7, convert unicode to utf-8
except ValueError:
validation_error = "'%s' is not a valid string array" % str(user_value)
error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
Expand Down Expand Up @@ -738,9 +719,6 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
elif Comparator.ARRAY_CONTAINS_ANY_OF <= comparator <= Comparator.ARRAY_NOT_CONTAINS_ANY_OF:
try:
user_value_list = self._get_user_attribute_value_as_string_list(user_value)

if sys.version_info[0] == 2:
user_value_list = unicode_to_utf8(user_value_list) # On Python 2.7, convert unicode to utf-8
except ValueError:
validation_error = "'%s' is not a valid string array" % str(user_value)
error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
Expand Down
41 changes: 4 additions & 37 deletions configcatclient/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
import inspect
from qualname import qualname
from datetime import datetime

try:
from datetime import timezone
except ImportError:
import pytz as timezone # On Python 2.7, datetime.timezone is not available. We use pytz instead.
from datetime import timezone

epoch_time = datetime(1970, 1, 1, tzinfo=timezone.utc)
distant_future = sys.float_info.max
Expand Down Expand Up @@ -88,40 +84,11 @@ def is_string_list(value):

# Check if all items in the list are strings
for item in value:
# Handle unicode strings on Python 2.7
if sys.version_info[0] == 2:
if not isinstance(item, (str, unicode)): # noqa: F821
return False
else:
if not isinstance(item, str):
return False
if not isinstance(item, str):
return False

return True


def unicode_to_utf8(data):
"""
Convert unicode data in a collection to UTF-8 data. Used for supporting unicode config json strings on Python 2.7.
Once Python 2.7 is no longer supported, this function can be removed.
"""
if isinstance(data, dict):
return {unicode_to_utf8(key): unicode_to_utf8(value) for key, value in data.iteritems()}
elif isinstance(data, list):
return [unicode_to_utf8(element) for element in data]
elif isinstance(data, unicode): # noqa: F821 (ignore warning: unicode is undefined in Python 3)
return data.encode('utf-8')
else:
return data


def encode_utf8(value):
"""
Get the UTF-8 encoded value of a string. Used for supporting unicode config json strings on Python 2.7.
If the value is already UTF-8 encoded, it is returned as is.
Once Python 2.7 is no longer supported, this function can be removed.
The use of this function can be replaced with encode() method of the string: value.encode('utf-8')
"""
try:
return value.encode('utf-8')
except UnicodeDecodeError:
return value
return value.encode('utf-8')
2 changes: 1 addition & 1 deletion configcatclient/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CONFIGCATCLIENT_VERSION = "9.0.4"
CONFIGCATCLIENT_VERSION = "10.0.0"
Loading

0 comments on commit ecda5e6

Please sign in to comment.