Skip to content

Commit 1e78ebe

Browse files
committed
Merge branch 'main' into feat/gherkin_and_testcontainers
2 parents 7c809e8 + a17c0e7 commit 1e78ebe

File tree

17 files changed

+582
-125
lines changed

17 files changed

+582
-125
lines changed

.github/workflows/build.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
runs-on: ubuntu-latest
2323
strategy:
2424
matrix:
25-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
25+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
2626
package:
2727
- "hooks/openfeature-hooks-opentelemetry"
2828
- "providers/openfeature-provider-flagd"
@@ -38,9 +38,10 @@ jobs:
3838
python-version: ${{ matrix.python-version }}
3939
cache: "pip"
4040

41-
- uses: bufbuild/buf-setup-action@v1.28.1
41+
- uses: bufbuild/buf-action@v1
4242
with:
4343
github_token: ${{ github.token }}
44+
setup_only: true
4445

4546
- name: Install hatch
4647
run: pip install hatch
@@ -49,8 +50,9 @@ jobs:
4950
run: hatch run cov
5051
working-directory: ${{ matrix.package }}
5152

52-
- name: Upload coverage to Codecov
53-
uses: codecov/[email protected]
53+
- if: matrix.python-version == '3.11'
54+
name: Upload coverage to Codecov
55+
uses: codecov/[email protected]
5456
with:
5557
name: Code Coverage for ${{ matrix.package }} on Python ${{ matrix.python-version }}
5658
directory: ${{ matrix.package }}
@@ -69,7 +71,7 @@ jobs:
6971
cache: "pip"
7072

7173
- name: Run pre-commit
72-
uses: pre-commit/[email protected].0
74+
uses: pre-commit/[email protected].1
7375

7476
sast:
7577
runs-on: ubuntu-latest

.pre-commit-config.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
default_stages: [commit]
22
repos:
33
- repo: https://github.com/astral-sh/ruff-pre-commit
4-
rev: v0.3.3
4+
rev: v0.6.3
55
hooks:
66
- id: ruff
77
args: [--fix]
88
- id: ruff-format
99

1010
- repo: https://github.com/pre-commit/pre-commit-hooks
11-
rev: v4.5.0
11+
rev: v4.6.0
1212
hooks:
1313
- id: check-toml
1414
- id: check-yaml
1515
- id: trailing-whitespace
1616
- id: check-merge-conflict
1717

1818
- repo: https://github.com/pre-commit/mirrors-mypy
19-
rev: v1.9.0
19+
rev: v1.11.2
2020
hooks:
2121
- id: mypy
2222
args: [--python-version=3.8]
@@ -25,6 +25,7 @@ repos:
2525
- opentelemetry-api
2626
- types-protobuf
2727
- types-PyYAML
28+
- types-requests
2829
- mmh3
2930
- semver
3031
- panzi-json-logic

.release-please-manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"hooks/openfeature-hooks-opentelemetry": "0.1.3",
3-
"providers/openfeature-provider-flagd": "0.1.5"
3+
"providers/openfeature-provider-flagd": "0.1.5",
4+
"providers/openfeature-provider-ofrep": "0.1.0"
45
}

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import typing
22

3-
from typing_extensions import Protocol
4-
53
from openfeature.evaluation_context import EvaluationContext
64
from openfeature.flag_evaluation import FlagResolutionDetails
75

86
from .grpc import GrpcResolver
97
from .in_process import InProcessResolver
108

119

12-
class AbstractResolver(Protocol):
10+
class AbstractResolver(typing.Protocol):
1311
def shutdown(self) -> None: ...
1412

1513
def resolve_boolean_details(

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import typing
3+
from dataclasses import dataclass
34

45
import mmh3
56
import semver
@@ -10,6 +11,12 @@
1011
logger = logging.getLogger("openfeature.contrib")
1112

1213

14+
@dataclass
15+
class Fraction:
16+
variant: str
17+
weight: int = 1
18+
19+
1320
def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
1421
if not args:
1522
logger.error("No arguments provided to fractional operator.")
@@ -32,28 +39,51 @@ def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
3239
return None
3340

3441
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
35-
bucket = int(hash_ratio * 100)
42+
bucket = hash_ratio * 100
3643

44+
total_weight = 0
45+
fractions = []
3746
for arg in args:
38-
if (
39-
not isinstance(arg, (tuple, list))
40-
or len(arg) != 2
41-
or not isinstance(arg[0], str)
42-
or not isinstance(arg[1], int)
43-
):
44-
logger.error("Fractional variant weights must be (str, int) tuple")
45-
return None
46-
variant_weights: typing.Tuple[typing.Tuple[str, int]] = args # type: ignore[assignment]
47-
48-
range_end = 0
49-
for variant, weight in variant_weights:
50-
range_end += weight
47+
fraction = _parse_fraction(arg)
48+
if fraction:
49+
fractions.append(fraction)
50+
total_weight += fraction.weight
51+
52+
range_end: float = 0
53+
for fraction in fractions:
54+
range_end += fraction.weight * 100 / total_weight
5155
if bucket < range_end:
52-
return variant
56+
return fraction.variant
5357

5458
return None
5559

5660

61+
def _parse_fraction(arg: JsonLogicArg) -> typing.Optional[Fraction]:
62+
if not isinstance(arg, (tuple, list)) or not arg:
63+
logger.error(
64+
"Fractional variant weights must be (str, int) tuple or [str] list"
65+
)
66+
return None
67+
68+
if not isinstance(arg[0], str):
69+
logger.error(
70+
"Fractional variant identifier (first element) isn't of type 'str'"
71+
)
72+
return None
73+
74+
if len(arg) >= 2 and not isinstance(arg[1], int):
75+
logger.error(
76+
"Fractional variant weight value (second element) isn't of type 'int'"
77+
)
78+
return None
79+
80+
fraction = Fraction(variant=arg[0])
81+
if len(arg) >= 2:
82+
fraction.weight = arg[1]
83+
84+
return fraction
85+
86+
5787
def starts_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
5888
def f(s1: str, s2: str) -> bool:
5989
return s1.startswith(s2)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
import typing
22

3+
import pytest
4+
from testcontainers.core.container import DockerContainer
5+
from tests.e2eGherkin.flagd_container import FlagDContainer
36
from tests.e2eGherkin.steps import * # noqa: F403
47

8+
from openfeature import api
9+
from openfeature.contrib.provider.flagd import FlagdProvider
10+
from openfeature.contrib.provider.flagd.config import ResolverType
11+
512
JsonPrimitive = typing.Union[str, bool, float, int]
13+
14+
15+
@pytest.fixture(autouse=True, scope="package")
16+
def setup(request, port, image):
17+
container: DockerContainer = FlagDContainer(
18+
image=image,
19+
port=port,
20+
)
21+
# Setup code
22+
c = container.start()
23+
api.set_provider(
24+
FlagdProvider(
25+
resolver_type=ResolverType.GRPC,
26+
port=int(container.get_exposed_port(port)),
27+
)
28+
)
29+
30+
def fin():
31+
c.stop()
32+
33+
# Teardown code
34+
request.addfinalizer(fin)
Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import time
2-
from time import sleep
32

4-
import grpc
5-
from grpc_health.v1 import health_pb2, health_pb2_grpc
63
from testcontainers.core.container import DockerContainer
74
from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs
85

@@ -12,7 +9,7 @@
129
class FlagDContainer(DockerContainer):
1310
def __init__(
1411
self,
15-
image: str = "ghcr.io/open-feature/flagd-testbed:v0.5.10",
12+
image: str = "ghcr.io/open-feature/flagd-testbed:v0.5.13",
1613
port: int = 8013,
1714
**kwargs,
1815
) -> None:
@@ -30,31 +27,31 @@ def _checker(self, host: str, port: int) -> None:
3027
# First we wait for Flagd to say it's listening
3128
wait_for_logs(
3229
self,
33-
"Flag IResolver listening at",
30+
"listening",
3431
5,
3532
)
3633

3734
time.sleep(1)
38-
# Second we use the GRPC health check endpoint
39-
with grpc.insecure_channel(host + ":" + port) as channel:
40-
health_stub = health_pb2_grpc.HealthStub(channel)
41-
42-
def health_check_call(stub: health_pb2_grpc.HealthStub):
43-
request = health_pb2.HealthCheckRequest()
44-
resp = stub.Check(request)
45-
if resp.status == health_pb2.HealthCheckResponse.SERVING:
46-
return True
47-
elif resp.status == health_pb2.HealthCheckResponse.NOT_SERVING:
48-
return False
49-
50-
# Should succeed
51-
# Check health status every 1 second for 30 seconds
52-
ok = False
53-
for _ in range(30):
54-
ok = health_check_call(health_stub)
55-
if ok:
56-
break
57-
sleep(1)
58-
59-
if not ok:
60-
raise ConnectionError("flagD not ready in time")
35+
# # Second we use the GRPC health check endpoint
36+
# with grpc.insecure_channel(host + ":" + port) as channel:
37+
# health_stub = health_pb2_grpc.HealthStub(channel)
38+
#
39+
# def health_check_call(stub: health_pb2_grpc.HealthStub):
40+
# request = health_pb2.HealthCheckRequest()
41+
# resp = stub.Check(request)
42+
# if resp.status == health_pb2.HealthCheckResponse.SERVING:
43+
# return True
44+
# elif resp.status == health_pb2.HealthCheckResponse.NOT_SERVING:
45+
# return False
46+
#
47+
# # Should succeed
48+
# # Check health status every 1 second for 30 seconds
49+
# ok = False
50+
# for _ in range(30):
51+
# ok = health_check_call(health_stub)
52+
# if ok:
53+
# break
54+
# sleep(1)
55+
#
56+
# if not ok:
57+
# raise ConnectionError("flagD not ready in time")

0 commit comments

Comments
 (0)