fast-feature-storage-sqlalchemy 0.0.1__py3-none-any.whl

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,15 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import Base
4
+ from .mapper import FlagMapper
5
+ from .model import FeatureFlagRow
6
+ from .repository import SqlAlchemyFlagRepository
7
+ from .schema import Schema
8
+
9
+ __all__ = [
10
+ "Base",
11
+ "FeatureFlagRow",
12
+ "FlagMapper",
13
+ "SqlAlchemyFlagRepository",
14
+ "Schema",
15
+ ]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.orm import DeclarativeBase
4
+
5
+
6
+ class Base(DeclarativeBase):
7
+ """Declarative base for fast-feature's SQLAlchemy models."""
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from fast_feature.core import Flag, FlagState
4
+
5
+ from .model import FeatureFlagRow
6
+
7
+
8
+ class FlagMapper:
9
+ """Translates between the domain ``Flag`` and the ORM row."""
10
+
11
+ @staticmethod
12
+ def to_row(flag: Flag) -> FeatureFlagRow:
13
+ return FeatureFlagRow(
14
+ key=flag.key,
15
+ variants=flag.variants,
16
+ default_variant=flag.default_variant,
17
+ state=flag.state.value,
18
+ targeting=flag.targeting,
19
+ flag_metadata=flag.metadata,
20
+ created_at=flag.created_at,
21
+ updated_at=flag.updated_at,
22
+ )
23
+
24
+ @staticmethod
25
+ def to_domain(row: FeatureFlagRow) -> Flag:
26
+ return Flag(
27
+ key=row.key,
28
+ variants=row.variants,
29
+ default_variant=row.default_variant,
30
+ state=FlagState(row.state),
31
+ targeting=row.targeting,
32
+ metadata=row.flag_metadata,
33
+ created_at=row.created_at,
34
+ updated_at=row.updated_at,
35
+ )
36
+
37
+ @staticmethod
38
+ def apply(row: FeatureFlagRow, flag: Flag) -> None:
39
+ row.variants = flag.variants
40
+ row.default_variant = flag.default_variant
41
+ row.state = flag.state.value
42
+ row.targeting = flag.targeting
43
+ row.flag_metadata = flag.metadata
44
+ row.created_at = flag.created_at
45
+ row.updated_at = flag.updated_at
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from sqlalchemy import JSON, DateTime, String
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+
9
+ from .base import Base
10
+
11
+
12
+ class FeatureFlagRow(Base):
13
+ __tablename__ = "feature_flags"
14
+
15
+ key: Mapped[str] = mapped_column(String(255), primary_key=True)
16
+ variants: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
17
+ default_variant: Mapped[str] = mapped_column(String(255), nullable=False)
18
+ state: Mapped[str] = mapped_column(String(32), nullable=False)
19
+ targeting: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
20
+ # "metadata" is reserved on declarative classes, so the attribute is renamed.
21
+ flag_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSON, nullable=False)
22
+ created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
23
+ updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
File without changes
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from fast_feature.core import Flag, FlagAlreadyExistsError, FlagNotFoundError, FlagRepository
4
+ from sqlalchemy import select
5
+ from sqlalchemy.exc import IntegrityError
6
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
7
+
8
+ from .mapper import FlagMapper
9
+ from .model import FeatureFlagRow
10
+
11
+
12
+ class SqlAlchemyFlagRepository(FlagRepository):
13
+ """A ``FlagRepository`` backed by an async SQLAlchemy session factory."""
14
+
15
+ def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
16
+ self._session_factory = session_factory
17
+
18
+ async def get(self, key: str) -> Flag | None:
19
+ async with self._session_factory() as session:
20
+ row = await session.get(FeatureFlagRow, key)
21
+ return FlagMapper.to_domain(row) if row is not None else None
22
+
23
+ async def list_all(self) -> list[Flag]:
24
+ async with self._session_factory() as session:
25
+ result = await session.execute(select(FeatureFlagRow).order_by(FeatureFlagRow.key))
26
+ return [FlagMapper.to_domain(row) for row in result.scalars()]
27
+
28
+ async def create(self, flag: Flag) -> Flag:
29
+ async with self._session_factory() as session:
30
+ session.add(FlagMapper.to_row(flag))
31
+ try:
32
+ await session.commit()
33
+ except IntegrityError as exc:
34
+ await session.rollback()
35
+ raise FlagAlreadyExistsError(flag.key) from exc
36
+ return flag
37
+
38
+ async def update(self, flag: Flag) -> Flag:
39
+ async with self._session_factory() as session:
40
+ row = await session.get(FeatureFlagRow, flag.key)
41
+ if row is None:
42
+ raise FlagNotFoundError(flag.key)
43
+ FlagMapper.apply(row, flag)
44
+ await session.commit()
45
+ return flag
46
+
47
+ async def delete(self, key: str) -> None:
48
+ async with self._session_factory() as session:
49
+ row = await session.get(FeatureFlagRow, key)
50
+ if row is None:
51
+ raise FlagNotFoundError(key)
52
+ await session.delete(row)
53
+ await session.commit()
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncEngine
4
+
5
+ from .base import Base
6
+
7
+
8
+ class Schema:
9
+ """Creates the database schema.
10
+
11
+ Convenient for development and tests; production deployments should manage
12
+ the ``feature_flags`` table with their own migration tooling.
13
+ """
14
+
15
+ @staticmethod
16
+ async def create_all(engine: AsyncEngine) -> None:
17
+ async with engine.begin() as connection:
18
+ await connection.run_sync(Base.metadata.create_all)
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-feature-storage-sqlalchemy
3
+ Version: 0.0.1
4
+ Summary: Async SQLAlchemy storage backend for fast-feature (shared by the SQL drivers).
5
+ Author: byunjuneseok
6
+ Author-email: byunjuneseok <byunjuneseok@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: fast-feature-core==0.0.1
18
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
19
+ Requires-Python: >=3.10
@@ -0,0 +1,10 @@
1
+ fast_feature/storage/sqlalchemy/__init__.py,sha256=_v1JLDRqd-5OSAszO2R1JPImiv7ez0Xl64MRTxv9wJc,313
2
+ fast_feature/storage/sqlalchemy/base.py,sha256=fBAuFeR2wxQhq-oxbcpyfpFhm2Oco2AKW32wfbcd_nI,175
3
+ fast_feature/storage/sqlalchemy/mapper.py,sha256=QwGU6IkazMXRDEUoFF-F0ZIbzex2Knj-JSfnn512tGE,1381
4
+ fast_feature/storage/sqlalchemy/model.py,sha256=RUIXzxRKA85t-W2AN_qXXh3arnkedeZxPlAo2PKDkks,982
5
+ fast_feature/storage/sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ fast_feature/storage/sqlalchemy/repository.py,sha256=H1TI5T3h-jb0eYqqWGdH_nax1K3sdctjfhSjKS5-SxY,2132
7
+ fast_feature/storage/sqlalchemy/schema.py,sha256=dgw5yhW8qJCWqDxEuOtS92oWRetlbqk7HIUQHvw65UU,500
8
+ fast_feature_storage_sqlalchemy-0.0.1.dist-info/WHEEL,sha256=8ZlpUMJ7mlDirmlHRhDirEx_nPnARrwDjeE92mlk68E,81
9
+ fast_feature_storage_sqlalchemy-0.0.1.dist-info/METADATA,sha256=GLQ2R-LVrFENckGKl27Uh0GgQxT9yqPRKYqqnHIBIaY,787
10
+ fast_feature_storage_sqlalchemy-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any