fast-feature-core 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-feature-core
3
+ Version: 0.0.1
4
+ Summary: Core domain model for fast-feature: flags, evaluation outcomes, and the storage interface.
5
+ Author: byunjuneseok
6
+ Author-email: byunjuneseok <byunjuneseok@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.10
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "fast-feature-core"
3
+ version = "0.0.1"
4
+ description = "Core domain model for fast-feature: flags, evaluation outcomes, and the storage interface."
5
+ license = "Apache-2.0"
6
+ requires-python = ">=3.10"
7
+ authors = [
8
+ { name = "byunjuneseok", email = "byunjuneseok@gmail.com" },
9
+ ]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: Apache Software License",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Typing :: Typed",
19
+ ]
20
+ # Pure-Python domain model with no runtime dependencies.
21
+ dependencies = []
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.11.17,<0.12.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [tool.uv.build-backend]
28
+ module-name = "fast_feature.core"
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from .errors import (
4
+ CoreError,
5
+ FlagAlreadyExistsError,
6
+ FlagNotFoundError,
7
+ InvalidFlagError,
8
+ )
9
+ from .evaluation import ErrorCode, EvaluationOutcome, Reason
10
+ from .flag import Flag, FlagState
11
+ from .repository import FlagRepository
12
+ from .types import EvaluationContext, JsonValue
13
+
14
+ __all__ = [
15
+ "EvaluationContext",
16
+ "JsonValue",
17
+ "Flag",
18
+ "FlagState",
19
+ "FlagRepository",
20
+ "EvaluationOutcome",
21
+ "Reason",
22
+ "ErrorCode",
23
+ "CoreError",
24
+ "FlagNotFoundError",
25
+ "FlagAlreadyExistsError",
26
+ "InvalidFlagError",
27
+ ]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class CoreError(Exception):
5
+ """Base class for all fast-feature-core errors.
6
+
7
+ Dependents catch this at their boundary and re-raise as their own base
8
+ error (with chaining) rather than coupling to the concrete subclasses.
9
+ """
10
+
11
+
12
+ class FlagNotFoundError(CoreError):
13
+ def __init__(self, key: str) -> None:
14
+ self.key = key
15
+ super().__init__(f"Flag {key!r} was not found")
16
+
17
+
18
+ class FlagAlreadyExistsError(CoreError):
19
+ def __init__(self, key: str) -> None:
20
+ self.key = key
21
+ super().__init__(f"Flag {key!r} already exists")
22
+
23
+
24
+ class InvalidFlagError(CoreError):
25
+ """Raised when a flag definition is structurally invalid."""
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from .types import JsonValue
7
+
8
+
9
+ class Reason(str, Enum):
10
+ """OpenFeature resolution reasons.
11
+
12
+ OFREP's core enum is a subset of these; the wider set is kept because the
13
+ OpenFeature specification treats ``reason`` as an open string.
14
+ """
15
+
16
+ STATIC = "STATIC"
17
+ DEFAULT = "DEFAULT"
18
+ TARGETING_MATCH = "TARGETING_MATCH"
19
+ SPLIT = "SPLIT"
20
+ DISABLED = "DISABLED"
21
+ CACHED = "CACHED"
22
+ UNKNOWN = "UNKNOWN"
23
+ ERROR = "ERROR"
24
+
25
+
26
+ class ErrorCode(str, Enum):
27
+ """OpenFeature error codes used when an evaluation fails."""
28
+
29
+ FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
30
+ PARSE_ERROR = "PARSE_ERROR"
31
+ TYPE_MISMATCH = "TYPE_MISMATCH"
32
+ INVALID_CONTEXT = "INVALID_CONTEXT"
33
+ TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
34
+ PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
35
+ GENERAL = "GENERAL"
36
+
37
+
38
+ @dataclass
39
+ class EvaluationOutcome:
40
+ """The result of evaluating a single flag against a context."""
41
+
42
+ key: str
43
+ reason: Reason
44
+ value: JsonValue = None
45
+ variant: str | None = None
46
+ metadata: dict[str, JsonValue] | None = None
47
+ error_code: ErrorCode | None = None
48
+ error_details: str | None = None
49
+
50
+ @property
51
+ def is_error(self) -> bool:
52
+ return self.error_code is not None
53
+
54
+ @classmethod
55
+ def error(
56
+ cls,
57
+ key: str,
58
+ error_code: ErrorCode,
59
+ details: str | None = None,
60
+ ) -> EvaluationOutcome:
61
+ return cls(
62
+ key=key,
63
+ reason=Reason.ERROR,
64
+ error_code=error_code,
65
+ error_details=details,
66
+ )
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from enum import Enum
7
+
8
+ from .errors import InvalidFlagError
9
+ from .types import JsonValue
10
+
11
+ _KEY_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+$")
12
+
13
+
14
+ class FlagState(str, Enum):
15
+ ENABLED = "ENABLED"
16
+ DISABLED = "DISABLED"
17
+
18
+
19
+ @dataclass
20
+ class Flag:
21
+ """A feature flag with named variants and an optional targeting rule.
22
+
23
+ A flag holds a set of named ``variants`` and resolves to exactly one of
24
+ them. Resolution is governed by an optional ``targeting`` rule (JsonLogic);
25
+ in its absence the ``default_variant`` is used.
26
+ """
27
+
28
+ key: str
29
+ variants: dict[str, JsonValue]
30
+ default_variant: str
31
+ state: FlagState = FlagState.ENABLED
32
+ targeting: dict[str, JsonValue] | None = None
33
+ metadata: dict[str, JsonValue] = field(default_factory=dict)
34
+ created_at: datetime | None = None
35
+ updated_at: datetime | None = None
36
+
37
+ def __post_init__(self) -> None:
38
+ if not _KEY_PATTERN.match(self.key):
39
+ raise InvalidFlagError(
40
+ f"Invalid flag key {self.key!r}: must match {_KEY_PATTERN.pattern}"
41
+ )
42
+ if not self.variants:
43
+ raise InvalidFlagError(f"Flag {self.key!r} must define at least one variant")
44
+ if self.default_variant not in self.variants:
45
+ raise InvalidFlagError(
46
+ f"Flag {self.key!r} default variant {self.default_variant!r} "
47
+ "is not among its variants"
48
+ )
49
+
50
+ @property
51
+ def default_value(self) -> JsonValue:
52
+ return self.variants[self.default_variant]
53
+
54
+ def value_of(self, variant: str) -> JsonValue:
55
+ return self.variants[variant]
56
+
57
+ def has_variant(self, variant: str) -> bool:
58
+ return variant in self.variants
File without changes
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from .flag import Flag
6
+
7
+
8
+ class FlagRepository(ABC):
9
+ """Storage boundary for flags.
10
+
11
+ Implementations live behind optional extras (``inmemory``, ``postgresql``,
12
+ ``mysql``). The domain and the evaluation engine depend only on this
13
+ interface, never on a concrete storage technology.
14
+ """
15
+
16
+ @abstractmethod
17
+ async def get(self, key: str) -> Flag | None:
18
+ """Return the flag with ``key``, or ``None`` if it does not exist."""
19
+
20
+ @abstractmethod
21
+ async def list_all(self) -> list[Flag]:
22
+ """Return every flag, ordered by key."""
23
+
24
+ @abstractmethod
25
+ async def create(self, flag: Flag) -> Flag:
26
+ """Persist a new flag.
27
+
28
+ Raises:
29
+ FlagAlreadyExistsError: if a flag with the same key exists.
30
+ """
31
+
32
+ @abstractmethod
33
+ async def update(self, flag: Flag) -> Flag:
34
+ """Replace an existing flag.
35
+
36
+ Raises:
37
+ FlagNotFoundError: if the flag does not exist.
38
+ """
39
+
40
+ @abstractmethod
41
+ async def delete(self, key: str) -> None:
42
+ """Remove a flag.
43
+
44
+ Raises:
45
+ FlagNotFoundError: if the flag does not exist.
46
+ """
47
+
48
+ async def exists(self, key: str) -> bool:
49
+ return await self.get(key) is not None
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TypeAlias
4
+
5
+ JsonValue: TypeAlias = "bool | int | float | str | None | list[JsonValue] | dict[str, JsonValue]"
6
+ """Any value expressible in JSON. Flag variants resolve to one of these."""
7
+
8
+ EvaluationContext: TypeAlias = "dict[str, JsonValue]"
9
+ """Targeting context supplied by a client at evaluation time.
10
+
11
+ By OpenFeature convention the optional ``targetingKey`` entry identifies the
12
+ subject of the evaluation (a user, account, session, ...).
13
+ """