fast-feature-storage-sqlalchemy 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.
- fast_feature_storage_sqlalchemy-0.0.1/PKG-INFO +19 -0
- fast_feature_storage_sqlalchemy-0.0.1/pyproject.toml +34 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/__init__.py +15 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/base.py +7 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/mapper.py +45 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/model.py +23 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/py.typed +0 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/repository.py +53 -0
- fast_feature_storage_sqlalchemy-0.0.1/src/fast_feature/storage/sqlalchemy/schema.py +18 -0
|
@@ -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,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fast-feature-storage-sqlalchemy"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Async SQLAlchemy storage backend for fast-feature (shared by the SQL drivers)."
|
|
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
|
+
"Framework :: AsyncIO",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: Apache Software License",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"fast-feature-core==0.0.1",
|
|
23
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.11.17,<0.12.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[tool.uv.build-backend]
|
|
31
|
+
module-name = "fast_feature.storage.sqlalchemy"
|
|
32
|
+
|
|
33
|
+
[tool.uv.sources]
|
|
34
|
+
fast-feature-core = { workspace = true }
|
|
@@ -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,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)
|