fennflow 0.1.0__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.
Files changed (113) hide show
  1. fennflow/__init__.py +7 -0
  2. fennflow/_base_exception.py +2 -0
  3. fennflow/_datetime.py +10 -0
  4. fennflow/_decorators.py +33 -0
  5. fennflow/_new_types.py +7 -0
  6. fennflow/_operations/__init__.py +0 -0
  7. fennflow/_operations/context/__init__.py +0 -0
  8. fennflow/_operations/context/abstract.py +6 -0
  9. fennflow/_operations/context/delete.py +15 -0
  10. fennflow/_operations/context/put.py +14 -0
  11. fennflow/_operations/context/types.py +7 -0
  12. fennflow/_operations/dto.py +79 -0
  13. fennflow/_operations/enums.py +19 -0
  14. fennflow/_operations/executor.py +47 -0
  15. fennflow/_operations/flows/__init__.py +0 -0
  16. fennflow/_operations/flows/abstract.py +33 -0
  17. fennflow/_operations/flows/delete.py +69 -0
  18. fennflow/_operations/flows/put.py +52 -0
  19. fennflow/_operations/registry.py +15 -0
  20. fennflow/_path.py +31 -0
  21. fennflow/_reconciler/__init__.py +10 -0
  22. fennflow/_reconciler/_orchestrator.py +86 -0
  23. fennflow/_reconciler/config.py +10 -0
  24. fennflow/_reconciler/core.py +112 -0
  25. fennflow/_reconciler/enums.py +29 -0
  26. fennflow/_reconciler/exceptions.py +6 -0
  27. fennflow/_resolver.py +62 -0
  28. fennflow/_sentinel.py +69 -0
  29. fennflow/backends/__init__.py +7 -0
  30. fennflow/backends/abstract/__init__.py +0 -0
  31. fennflow/backends/abstract/annotations.py +30 -0
  32. fennflow/backends/abstract/base.py +95 -0
  33. fennflow/backends/abstract/config.py +15 -0
  34. fennflow/backends/enums.py +9 -0
  35. fennflow/backends/exceptions.py +9 -0
  36. fennflow/backends/factory.py +22 -0
  37. fennflow/backends/in_memory/__init__.py +7 -0
  38. fennflow/backends/in_memory/_select.py +124 -0
  39. fennflow/backends/in_memory/base.py +214 -0
  40. fennflow/backends/in_memory/config.py +9 -0
  41. fennflow/backends/registry.py +5 -0
  42. fennflow/backends/responses.py +33 -0
  43. fennflow/backends/types/__init__.py +0 -0
  44. fennflow/backends/types/config.py +5 -0
  45. fennflow/connectors/__init__.py +11 -0
  46. fennflow/connectors/abstract/__init__.py +3 -0
  47. fennflow/connectors/abstract/base.py +88 -0
  48. fennflow/connectors/factory.py +40 -0
  49. fennflow/connectors/in_memory/__init__.py +7 -0
  50. fennflow/connectors/in_memory/config.py +9 -0
  51. fennflow/connectors/in_memory/connector.py +152 -0
  52. fennflow/connectors/registry.py +9 -0
  53. fennflow/connectors/s3/__init__.py +7 -0
  54. fennflow/connectors/s3/client.py +76 -0
  55. fennflow/connectors/s3/config.py +43 -0
  56. fennflow/connectors/s3/connector.py +182 -0
  57. fennflow/connectors/types/__init__.py +0 -0
  58. fennflow/connectors/types/config.py +6 -0
  59. fennflow/core/__init__.py +5 -0
  60. fennflow/core/configs/__init__.py +0 -0
  61. fennflow/core/configs/base.py +28 -0
  62. fennflow/exceptions/__init__.py +28 -0
  63. fennflow/files/__init__.py +23 -0
  64. fennflow/files/exceptions/__init__.py +18 -0
  65. fennflow/files/exceptions/base.py +5 -0
  66. fennflow/files/exceptions/cannot_parse_extension.py +13 -0
  67. fennflow/files/exceptions/extension_cannot_be_guessed.py +5 -0
  68. fennflow/files/exceptions/filename_and_mediatype_both_none.py +12 -0
  69. fennflow/files/exceptions/filename_is_none.py +8 -0
  70. fennflow/files/exceptions/media_type_cannot_be_guessed.py +23 -0
  71. fennflow/files/exceptions/storage_prefix_is_none.py +8 -0
  72. fennflow/files/factory.py +115 -0
  73. fennflow/files/media/__init__.py +21 -0
  74. fennflow/files/media/abstract/__init__.py +0 -0
  75. fennflow/files/media/abstract/content.py +21 -0
  76. fennflow/files/media/abstract/from_content.py +41 -0
  77. fennflow/files/media/audio_content.py +11 -0
  78. fennflow/files/media/base.py +163 -0
  79. fennflow/files/media/binary_content.py +32 -0
  80. fennflow/files/media/image_content.py +13 -0
  81. fennflow/files/media/json_content.py +71 -0
  82. fennflow/files/media/pdf_content.py +5 -0
  83. fennflow/files/media/text_content.py +62 -0
  84. fennflow/files/media/url_content.py +11 -0
  85. fennflow/files/media/video_content.py +15 -0
  86. fennflow/files/protocols/__init__.py +0 -0
  87. fennflow/files/protocols/has_data.py +15 -0
  88. fennflow/files/protocols/has_data_and_ext.py +20 -0
  89. fennflow/files/protocols/has_data_and_ext_obj.py +19 -0
  90. fennflow/files/protocols/has_ext.py +20 -0
  91. fennflow/files/registry.py +21 -0
  92. fennflow/files/responses/__init__.py +0 -0
  93. fennflow/files/responses/base.py +127 -0
  94. fennflow/files/responses/list.py +45 -0
  95. fennflow/files/types.py +26 -0
  96. fennflow/py.typed +0 -0
  97. fennflow/repositories/__init__.py +14 -0
  98. fennflow/repositories/at.py +60 -0
  99. fennflow/repositories/base.py +30 -0
  100. fennflow/repositories/delete.py +59 -0
  101. fennflow/repositories/fields/__init__.py +7 -0
  102. fennflow/repositories/fields/base.py +81 -0
  103. fennflow/repositories/fields/s3.py +44 -0
  104. fennflow/repositories/get.py +73 -0
  105. fennflow/repositories/list.py +57 -0
  106. fennflow/repositories/put.py +70 -0
  107. fennflow/uow/__init__.py +5 -0
  108. fennflow/uow/core.py +209 -0
  109. fennflow/uow/inspector.py +23 -0
  110. fennflow-0.1.0.dist-info/METADATA +162 -0
  111. fennflow-0.1.0.dist-info/RECORD +113 -0
  112. fennflow-0.1.0.dist-info/WHEEL +4 -0
  113. fennflow-0.1.0.dist-info/licenses/LICENSE +21 -0
fennflow/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "ConfigDict",
3
+ "UnitOfWork",
4
+ ]
5
+
6
+ from .core import ConfigDict
7
+ from .uow import UnitOfWork
@@ -0,0 +1,2 @@
1
+ class FennFlowException(Exception):
2
+ """Base exception for all fennflow errors."""
fennflow/_datetime.py ADDED
@@ -0,0 +1,10 @@
1
+ import datetime
2
+
3
+ from pydantic import AwareDatetime as PydanticAwareDatetime
4
+
5
+ AwareDatetime = PydanticAwareDatetime
6
+
7
+
8
+ def now() -> AwareDatetime:
9
+ """Returns a timezone-aware datetime object for current time."""
10
+ return datetime.datetime.now(tz=datetime.timezone.utc)
@@ -0,0 +1,33 @@
1
+ import asyncio
2
+ import functools
3
+ from collections.abc import Callable
4
+ from typing import Any, TypeAlias
5
+
6
+ CatchType: TypeAlias = type[Exception] | tuple[type[Exception], ...]
7
+
8
+
9
+ def reraise_with(
10
+ exception: Exception,
11
+ catch: CatchType = Exception,
12
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
13
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
14
+
15
+ @functools.wraps(func)
16
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
17
+ try:
18
+ return await func(*args, **kwargs)
19
+ except catch as e:
20
+ raise exception from e
21
+
22
+ @functools.wraps(func)
23
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
24
+ try:
25
+ return func(*args, **kwargs)
26
+ except catch as e:
27
+ raise exception from e
28
+
29
+ if asyncio.iscoroutinefunction(func):
30
+ return async_wrapper
31
+ return sync_wrapper
32
+
33
+ return decorator
fennflow/_new_types.py ADDED
@@ -0,0 +1,7 @@
1
+ from typing_extensions import TypeAliasType
2
+
3
+ UowQualName = TypeAliasType("UowQualName", str)
4
+ StoragePath = TypeAliasType("StoragePath", str)
5
+ Namespace = TypeAliasType("Namespace", str)
6
+ BackendScope = TypeAliasType("BackendScope", str)
7
+ BucketName = Namespace
File without changes
File without changes
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(slots=True)
5
+ class BaseContext:
6
+ pass
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ from fennflow._operations.context.abstract import BaseContext
7
+
8
+ if TYPE_CHECKING:
9
+ from fennflow._new_types import Namespace, StoragePath
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class DeleteContext(BaseContext):
14
+ to_storage_path: StoragePath
15
+ to_namespace: Namespace
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ from fennflow._operations.context.abstract import BaseContext
7
+
8
+ if TYPE_CHECKING:
9
+ from fennflow.files.types import BinaryMedia
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class PutContext(BaseContext):
14
+ file: BinaryMedia
@@ -0,0 +1,7 @@
1
+ from typing import TypeAlias
2
+
3
+ from fennflow._operations.context.abstract import BaseContext
4
+ from fennflow._operations.context.delete import DeleteContext
5
+ from fennflow._operations.context.put import PutContext
6
+
7
+ Context: TypeAlias = PutContext | DeleteContext | BaseContext
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+ from uuid import UUID
8
+
9
+ from fennflow._datetime import now
10
+ from fennflow._operations.context.abstract import BaseContext
11
+ from fennflow._operations.enums import OperationStatusEnum, OperationTypeEnum
12
+ from fennflow._sentinel import NOT_GIVEN
13
+
14
+ if TYPE_CHECKING:
15
+ from fennflow._new_types import Namespace, StoragePath
16
+ from fennflow._operations.context.types import Context
17
+ from fennflow._sentinel import NotGiven
18
+ from fennflow.repositories.fields.base import RepoExtra
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class OperationRecord:
23
+ session_id: uuid.UUID
24
+ storage_path: StoragePath
25
+ repo_extra: RepoExtra
26
+ operation_type: OperationTypeEnum
27
+ status: OperationStatusEnum
28
+ context: Context = field(default_factory=BaseContext)
29
+ operation_id: uuid.UUID = field(default_factory=uuid.uuid4)
30
+
31
+ created_at: datetime.datetime = field(
32
+ default_factory=now,
33
+ )
34
+ expired_at: datetime.datetime = field(
35
+ default_factory=lambda: now() + datetime.timedelta(seconds=30)
36
+ )
37
+ error: str | None | NotGiven = NOT_GIVEN
38
+
39
+ @property
40
+ def is_pending(self) -> bool:
41
+ return self.status == OperationStatusEnum.PENDING
42
+
43
+ @property
44
+ def is_uploaded(self) -> bool:
45
+ return self.status == OperationStatusEnum.UPLOADED
46
+
47
+ @property
48
+ def is_failed(self) -> bool:
49
+ return self.status == OperationStatusEnum.FAILED
50
+
51
+ @property
52
+ def is_deleted(self) -> bool:
53
+ return self.status == OperationStatusEnum.DELETED
54
+
55
+ @property
56
+ def namespace(self) -> Namespace:
57
+ return self.repo_extra["namespace"]
58
+
59
+ @property
60
+ def is_expired(self):
61
+ return self.expired_at < now()
62
+
63
+ @property
64
+ def is_dangling(self) -> bool:
65
+ return self.is_expired and not self.is_uploaded
66
+
67
+ def is_locked(self, current_session_id: UUID) -> bool:
68
+ return (
69
+ self.session_id != current_session_id
70
+ and self.is_pending
71
+ and not self.is_expired
72
+ )
73
+
74
+ def is_visible(self, requested_from_session_id: UUID) -> bool:
75
+ return self.is_uploaded or (
76
+ self.is_pending
77
+ and self.session_id == requested_from_session_id
78
+ and self.operation_type == OperationTypeEnum.PUT
79
+ )
@@ -0,0 +1,19 @@
1
+ from enum import IntEnum, auto
2
+
3
+
4
+ class OperationTypeEnum(IntEnum):
5
+ """Enum for operation types."""
6
+
7
+ PUT = auto()
8
+ GET = auto()
9
+ DELETE = auto()
10
+
11
+
12
+ class OperationStatusEnum(IntEnum):
13
+ """Enum for operation status."""
14
+
15
+ PENDING = auto()
16
+ UPLOADED = auto()
17
+ FAILED = auto()
18
+ COMPENSATION_FAILED = auto()
19
+ DELETED = auto()
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .registry import flow_registry
6
+
7
+ if TYPE_CHECKING:
8
+ from fennflow.connectors.abstract import AbstractConnector
9
+
10
+ from .dto import OperationRecord
11
+
12
+
13
+ class OperationExecutor:
14
+ def __init__(self, connector: AbstractConnector) -> None:
15
+ self.connector = connector
16
+
17
+ async def execute(
18
+ self,
19
+ operation: OperationRecord,
20
+ **provider_extra,
21
+ ):
22
+ operation_flow = flow_registry[operation.operation_type]
23
+ return await operation_flow().execute(
24
+ operation=operation,
25
+ connector=self.connector,
26
+ **provider_extra,
27
+ )
28
+
29
+ async def compensate(
30
+ self,
31
+ operation: OperationRecord,
32
+ ):
33
+ operation_flow = flow_registry[operation.operation_type]
34
+ return await operation_flow().compensate(
35
+ operation=operation,
36
+ connector=self.connector,
37
+ )
38
+
39
+ async def finalize(
40
+ self,
41
+ operation: OperationRecord,
42
+ ):
43
+ operation_flow = flow_registry[operation.operation_type]
44
+ return await operation_flow().finalize(
45
+ operation=operation,
46
+ connector=self.connector,
47
+ )
File without changes
@@ -0,0 +1,33 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from fennflow._operations.dto import OperationRecord
6
+ from fennflow.connectors.abstract import AbstractConnector
7
+
8
+
9
+ class AbstractFlow(ABC):
10
+ @staticmethod
11
+ @abstractmethod
12
+ async def execute(
13
+ *,
14
+ operation: "OperationRecord",
15
+ connector: "AbstractConnector",
16
+ **provider_extra,
17
+ ): ...
18
+
19
+ @staticmethod
20
+ @abstractmethod
21
+ async def compensate(
22
+ *,
23
+ operation: "OperationRecord",
24
+ connector: "AbstractConnector",
25
+ ): ...
26
+
27
+ @staticmethod
28
+ @abstractmethod
29
+ async def finalize(
30
+ *,
31
+ operation: "OperationRecord",
32
+ connector: "AbstractConnector",
33
+ ): ...
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from fennflow._operations.enums import OperationStatusEnum
6
+ from fennflow._operations.flows.abstract import AbstractFlow
7
+
8
+ if TYPE_CHECKING:
9
+ from fennflow._operations.context.delete import DeleteContext
10
+ from fennflow._operations.dto import OperationRecord
11
+ from fennflow.connectors.abstract import AbstractConnector
12
+
13
+
14
+ class DeleteFlow(AbstractFlow):
15
+ @staticmethod
16
+ async def execute(
17
+ *,
18
+ operation: OperationRecord,
19
+ connector: AbstractConnector,
20
+ **provider_extra,
21
+ ):
22
+ ctx: DeleteContext = operation.context
23
+ await connector.copy_object(
24
+ from_storage_path=operation.storage_path,
25
+ to_storage_path=ctx.to_storage_path,
26
+ to_namespace=ctx.to_namespace,
27
+ repo_extra=operation.repo_extra,
28
+ **provider_extra,
29
+ )
30
+ return await connector.delete(
31
+ storage_path=operation.storage_path,
32
+ repo_extra=operation.repo_extra,
33
+ **provider_extra,
34
+ )
35
+
36
+ @staticmethod
37
+ async def compensate(
38
+ *,
39
+ operation: OperationRecord,
40
+ connector: AbstractConnector,
41
+ **provider_extra,
42
+ ):
43
+ ctx: DeleteContext = operation.context
44
+ await connector.copy_object(
45
+ from_storage_path=ctx.to_storage_path,
46
+ to_storage_path=operation.storage_path,
47
+ to_namespace=operation.namespace,
48
+ repo_extra=operation.repo_extra,
49
+ **provider_extra,
50
+ )
51
+ await connector.delete(
52
+ storage_path=ctx.to_storage_path,
53
+ repo_extra=operation.repo_extra,
54
+ )
55
+ operation.status = OperationStatusEnum.UPLOADED
56
+
57
+ @staticmethod
58
+ async def finalize(
59
+ *,
60
+ operation: OperationRecord,
61
+ connector: AbstractConnector,
62
+ **provider_extra,
63
+ ):
64
+ ctx: DeleteContext = operation.context
65
+ await connector.delete(
66
+ storage_path=ctx.to_storage_path,
67
+ repo_extra=operation.repo_extra,
68
+ **provider_extra,
69
+ )
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from fennflow._operations.enums import OperationStatusEnum
6
+ from fennflow._operations.flows.abstract import AbstractFlow
7
+
8
+ if TYPE_CHECKING:
9
+ from fennflow._operations.context.put import PutContext
10
+ from fennflow._operations.dto import OperationRecord
11
+ from fennflow.connectors.abstract import AbstractConnector
12
+
13
+
14
+ class PutFlow(AbstractFlow):
15
+ @staticmethod
16
+ async def execute(
17
+ *,
18
+ operation: OperationRecord,
19
+ connector: AbstractConnector,
20
+ **provider_extra,
21
+ ):
22
+ ctx: PutContext = operation.context
23
+ return await connector.put(
24
+ file=ctx.file,
25
+ repo_extra=operation.repo_extra,
26
+ **provider_extra,
27
+ )
28
+
29
+ @staticmethod
30
+ async def compensate(
31
+ *,
32
+ operation: OperationRecord,
33
+ connector: AbstractConnector,
34
+ **provider_extra,
35
+ ):
36
+
37
+ result = await connector.delete(
38
+ storage_path=operation.storage_path,
39
+ repo_extra=operation.repo_extra,
40
+ **provider_extra,
41
+ )
42
+ operation.status = OperationStatusEnum.FAILED
43
+ return result
44
+
45
+ @staticmethod
46
+ async def finalize(
47
+ *,
48
+ operation: OperationRecord,
49
+ connector: AbstractConnector,
50
+ **provider_extra,
51
+ ):
52
+ pass
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .enums import OperationTypeEnum
6
+ from .flows.delete import DeleteFlow
7
+ from .flows.put import PutFlow
8
+
9
+ if TYPE_CHECKING:
10
+ from .flows.abstract import AbstractFlow
11
+
12
+ flow_registry: dict[OperationTypeEnum | str, type[AbstractFlow]] = {
13
+ OperationTypeEnum.PUT: PutFlow,
14
+ OperationTypeEnum.DELETE: DeleteFlow,
15
+ }
fennflow/_path.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from fennflow._new_types import StoragePath
8
+
9
+
10
+ class Path:
11
+ @staticmethod
12
+ def normalize_path(path: str) -> StoragePath:
13
+ path = re.sub(r"/+", "/", path)
14
+ return path.strip("/")
15
+
16
+ @classmethod
17
+ def join_path(cls, *paths: str) -> StoragePath:
18
+ path = "/".join(path for path in paths)
19
+
20
+ return cls.normalize_path(path)
21
+
22
+ @classmethod
23
+ def normalize_folder(cls, path: str) -> str:
24
+ """Used for display of folder path.
25
+
26
+ Adds trailing slash to distinguish folders from file keys.
27
+ _path itself has no trailing slash as it's used directly in join operations.
28
+
29
+ """
30
+ normalized = cls.normalize_path(path)
31
+ return normalized + "/" if normalized else ""
@@ -0,0 +1,10 @@
1
+ __all__ = [
2
+ "ReconcileConfig",
3
+ "ReconcileFrequencyEnum",
4
+ "ReconcileStrategyEnum",
5
+ "Reconciler",
6
+ ]
7
+
8
+ from .config import ReconcileConfig
9
+ from .core import Reconciler
10
+ from .enums import ReconcileFrequencyEnum, ReconcileStrategyEnum
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import TYPE_CHECKING, Any, ClassVar
6
+
7
+ from fennflow._reconciler.core import Reconciler
8
+ from fennflow._reconciler.enums import ReconcileFrequencyEnum
9
+ from fennflow.uow.inspector import UowInspector
10
+
11
+ if TYPE_CHECKING:
12
+ from fennflow import UnitOfWork
13
+ from fennflow._new_types import UowQualName
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ReconcileOrchestrator:
19
+ _reconciled_on_startup: ClassVar[set[UowQualName]] = set()
20
+ _lock: asyncio.Lock = asyncio.Lock()
21
+
22
+ async def reconcile_if_needed(self, uow: UnitOfWork) -> None:
23
+ frequency = uow._resolved_config.reconcile.frequency
24
+
25
+ async with self._lock:
26
+ match frequency:
27
+ case ReconcileFrequencyEnum.NEVER:
28
+ await self._handle_never(uow=uow)
29
+ case ReconcileFrequencyEnum.ON_START_APP:
30
+ await self._handle_on_start_app(uow=uow)
31
+ case ReconcileFrequencyEnum.ON_SESSION_START:
32
+ await self._handle_on_session_start(uow=uow)
33
+ case _:
34
+ raise AssertionError("Unhandled frequency")
35
+
36
+ async def _handle_never(self, uow: UnitOfWork) -> None:
37
+ logger.debug(
38
+ "Skipping reconciliation.",
39
+ extra=self._log_extra(uow),
40
+ )
41
+
42
+ async def _handle_on_start_app(self, uow: UnitOfWork) -> None:
43
+ if uow.__class__.__qualname__ in self._reconciled_on_startup:
44
+ logger.debug(
45
+ "Skipping reconciliation.",
46
+ extra=self._log_extra(uow=uow),
47
+ )
48
+ return
49
+
50
+ logger.debug(
51
+ "Starting reconciliation...",
52
+ extra=self._log_extra(uow=uow),
53
+ )
54
+ await self._reconcile(uow=uow)
55
+ self._reconciled_on_startup.add(uow.__class__.__qualname__)
56
+
57
+ async def _handle_on_session_start(self, uow: UnitOfWork) -> None:
58
+ logger.debug("Starting reconciliation...", extra=self._log_extra(uow=uow))
59
+ await self._reconcile(uow=uow)
60
+
61
+ async def _reconcile(self, uow: UnitOfWork) -> None:
62
+ extractor = UowInspector(uow=uow)
63
+ reconcile = Reconciler(
64
+ uow_fields=extractor.get_repo_fields(),
65
+ connector=uow.connector,
66
+ backend=uow.backend,
67
+ )
68
+
69
+ await reconcile.reconcile(
70
+ session_id=uow._session_id,
71
+ batch_size=uow._resolved_config.reconcile.batch_size,
72
+ strategy=uow._resolved_config.reconcile.strategy,
73
+ )
74
+
75
+ logger.debug(
76
+ "Finished reconciliation.",
77
+ extra=self._log_extra(uow=uow),
78
+ )
79
+
80
+ @staticmethod
81
+ def _log_extra(uow: UnitOfWork) -> dict[str, Any]:
82
+ return {
83
+ "session_id": uow._session_id,
84
+ "uow_class_name": uow.__class__.__qualname__,
85
+ "frequency": uow._resolved_config.reconcile.frequency,
86
+ }
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fennflow._reconciler.enums import ReconcileFrequencyEnum, ReconcileStrategyEnum
4
+
5
+
6
+ @dataclass(slots=True, frozen=True)
7
+ class ReconcileConfig:
8
+ frequency: ReconcileFrequencyEnum = ReconcileFrequencyEnum.ON_START_APP
9
+ strategy: ReconcileStrategyEnum = ReconcileStrategyEnum.FILL_IF_EMPTY
10
+ batch_size: int = 1000