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.
- fennflow/__init__.py +7 -0
- fennflow/_base_exception.py +2 -0
- fennflow/_datetime.py +10 -0
- fennflow/_decorators.py +33 -0
- fennflow/_new_types.py +7 -0
- fennflow/_operations/__init__.py +0 -0
- fennflow/_operations/context/__init__.py +0 -0
- fennflow/_operations/context/abstract.py +6 -0
- fennflow/_operations/context/delete.py +15 -0
- fennflow/_operations/context/put.py +14 -0
- fennflow/_operations/context/types.py +7 -0
- fennflow/_operations/dto.py +79 -0
- fennflow/_operations/enums.py +19 -0
- fennflow/_operations/executor.py +47 -0
- fennflow/_operations/flows/__init__.py +0 -0
- fennflow/_operations/flows/abstract.py +33 -0
- fennflow/_operations/flows/delete.py +69 -0
- fennflow/_operations/flows/put.py +52 -0
- fennflow/_operations/registry.py +15 -0
- fennflow/_path.py +31 -0
- fennflow/_reconciler/__init__.py +10 -0
- fennflow/_reconciler/_orchestrator.py +86 -0
- fennflow/_reconciler/config.py +10 -0
- fennflow/_reconciler/core.py +112 -0
- fennflow/_reconciler/enums.py +29 -0
- fennflow/_reconciler/exceptions.py +6 -0
- fennflow/_resolver.py +62 -0
- fennflow/_sentinel.py +69 -0
- fennflow/backends/__init__.py +7 -0
- fennflow/backends/abstract/__init__.py +0 -0
- fennflow/backends/abstract/annotations.py +30 -0
- fennflow/backends/abstract/base.py +95 -0
- fennflow/backends/abstract/config.py +15 -0
- fennflow/backends/enums.py +9 -0
- fennflow/backends/exceptions.py +9 -0
- fennflow/backends/factory.py +22 -0
- fennflow/backends/in_memory/__init__.py +7 -0
- fennflow/backends/in_memory/_select.py +124 -0
- fennflow/backends/in_memory/base.py +214 -0
- fennflow/backends/in_memory/config.py +9 -0
- fennflow/backends/registry.py +5 -0
- fennflow/backends/responses.py +33 -0
- fennflow/backends/types/__init__.py +0 -0
- fennflow/backends/types/config.py +5 -0
- fennflow/connectors/__init__.py +11 -0
- fennflow/connectors/abstract/__init__.py +3 -0
- fennflow/connectors/abstract/base.py +88 -0
- fennflow/connectors/factory.py +40 -0
- fennflow/connectors/in_memory/__init__.py +7 -0
- fennflow/connectors/in_memory/config.py +9 -0
- fennflow/connectors/in_memory/connector.py +152 -0
- fennflow/connectors/registry.py +9 -0
- fennflow/connectors/s3/__init__.py +7 -0
- fennflow/connectors/s3/client.py +76 -0
- fennflow/connectors/s3/config.py +43 -0
- fennflow/connectors/s3/connector.py +182 -0
- fennflow/connectors/types/__init__.py +0 -0
- fennflow/connectors/types/config.py +6 -0
- fennflow/core/__init__.py +5 -0
- fennflow/core/configs/__init__.py +0 -0
- fennflow/core/configs/base.py +28 -0
- fennflow/exceptions/__init__.py +28 -0
- fennflow/files/__init__.py +23 -0
- fennflow/files/exceptions/__init__.py +18 -0
- fennflow/files/exceptions/base.py +5 -0
- fennflow/files/exceptions/cannot_parse_extension.py +13 -0
- fennflow/files/exceptions/extension_cannot_be_guessed.py +5 -0
- fennflow/files/exceptions/filename_and_mediatype_both_none.py +12 -0
- fennflow/files/exceptions/filename_is_none.py +8 -0
- fennflow/files/exceptions/media_type_cannot_be_guessed.py +23 -0
- fennflow/files/exceptions/storage_prefix_is_none.py +8 -0
- fennflow/files/factory.py +115 -0
- fennflow/files/media/__init__.py +21 -0
- fennflow/files/media/abstract/__init__.py +0 -0
- fennflow/files/media/abstract/content.py +21 -0
- fennflow/files/media/abstract/from_content.py +41 -0
- fennflow/files/media/audio_content.py +11 -0
- fennflow/files/media/base.py +163 -0
- fennflow/files/media/binary_content.py +32 -0
- fennflow/files/media/image_content.py +13 -0
- fennflow/files/media/json_content.py +71 -0
- fennflow/files/media/pdf_content.py +5 -0
- fennflow/files/media/text_content.py +62 -0
- fennflow/files/media/url_content.py +11 -0
- fennflow/files/media/video_content.py +15 -0
- fennflow/files/protocols/__init__.py +0 -0
- fennflow/files/protocols/has_data.py +15 -0
- fennflow/files/protocols/has_data_and_ext.py +20 -0
- fennflow/files/protocols/has_data_and_ext_obj.py +19 -0
- fennflow/files/protocols/has_ext.py +20 -0
- fennflow/files/registry.py +21 -0
- fennflow/files/responses/__init__.py +0 -0
- fennflow/files/responses/base.py +127 -0
- fennflow/files/responses/list.py +45 -0
- fennflow/files/types.py +26 -0
- fennflow/py.typed +0 -0
- fennflow/repositories/__init__.py +14 -0
- fennflow/repositories/at.py +60 -0
- fennflow/repositories/base.py +30 -0
- fennflow/repositories/delete.py +59 -0
- fennflow/repositories/fields/__init__.py +7 -0
- fennflow/repositories/fields/base.py +81 -0
- fennflow/repositories/fields/s3.py +44 -0
- fennflow/repositories/get.py +73 -0
- fennflow/repositories/list.py +57 -0
- fennflow/repositories/put.py +70 -0
- fennflow/uow/__init__.py +5 -0
- fennflow/uow/core.py +209 -0
- fennflow/uow/inspector.py +23 -0
- fennflow-0.1.0.dist-info/METADATA +162 -0
- fennflow-0.1.0.dist-info/RECORD +113 -0
- fennflow-0.1.0.dist-info/WHEEL +4 -0
- fennflow-0.1.0.dist-info/licenses/LICENSE +21 -0
fennflow/__init__.py
ADDED
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)
|
fennflow/_decorators.py
ADDED
|
@@ -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,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,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
|