API
UnitOfWork
Unit of Work (UOW) — the main entry point of FennFlow.
It coordinates file operations by managing: - backend (operation metadata storage) - connector (actual storage, e.g. S3) - execution and compensation logic (Saga pattern)
Example::
class UOW(UnitOfWork):
config = ConfigDict(
backend=PostgresBackendConfig(...),
connector=S3ConnectorConfig(...),
)
user_files = S3RepoField(UserFiles, bucket_name="bucket_name")
# or
# user_files = RepoField(UserFiles, namespace="bucket_name")
async with UOW() as uow:
await uow.user_files.at("user1/").put(file)
Behavior:
- By default, auto_commit=True:
commits all operations if the context exits successfully
- If an exception occurs or auto_commit=False:
triggers rollback with compensation logic
Important: - Users should NOT interact with backend or connector directly - All operations must go through UOW - Rollback applies compensation in reverse order (Saga pattern)
Source code in src/fennflow/uow/core.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
backend
property
Direct access to the backend for read-only inspection.
Warning: mutating backend state directly bypasses Saga guarantees. Use UoW methods for all write operations.
connector
property
Direct access to the connector.
Warning: operations performed directly on the connector are not tracked by the backend. Therefore, they will not be compensated by uow.
commit()
async
Persists operation state via backend.
Source code in src/fennflow/uow/core.py
149 150 151 152 153 154 155 156 157 158 159 160 161 | |
rollback()
async
Performs rollback for saga flow.
Runs compensation for all pending operations and then rolls back backend state.
Source code in src/fennflow/uow/core.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | |
UowInspector
dataclass
Extracts info from Unit of Work.
Source code in src/fennflow/uow/inspector.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
get_repo_fields()
Yields repo fields.
Example::
class UOW(UnitOfWork):
images = S3RepoField(...)
videos: S3RepoField(...)
extractor = UowInspector(uow=uow)
extractor.get_repo_fields() # yield images, then videos fields
Source code in src/fennflow/uow/inspector.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
ConfigDict
Bases: TypedDict
Configuration for a UnitOfWork instance.
All fields are optional — if not provided, defaults are used.
Attributes:
| Name | Type | Description |
|---|---|---|
backend |
BackendConfig
|
Configuration for the metadata backend
(e.g. |
connector |
ConnectorConfig
|
Configuration for the storage connector (e.g. |
Example::
class UOW(UnitOfWork):
config = ConfigDict(
backend=SqlalchemyBackendConfig(),
connector=S3ConnectorConfig(),
)
Source code in src/fennflow/core/configs/base.py
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
InMemoryBackendConfig
Bases: SqlalchemyBackendConfig
Configuration for the in-memory SQLite backend.
Source code in src/fennflow/backends/in_memory/config.py
8 9 10 11 12 13 14 | |
SqlalchemyBackendConfig
Bases: AbstractBackendConfig
Configuration for the SqlAlchemy backend.
No configuration is required — the in-memory backend is zero-dependency.
Source code in src/fennflow/backends/sqlalchemy/config.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
ConnectorFactory
Factory for creating connector instances from config objects.
Resolves the appropriate connector class from connector_registry
based on the config class name.
Example::
connector = ConnectorFactory.from_config(S3ConnectorConfig(...))
Source code in src/fennflow/connectors/_factory.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
from_config(config)
staticmethod
Create a connector instance from a config object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
ConnectorConfig
|
The connector configuration instance. |
required |
Returns:
| Type | Description |
|---|---|
AbstractConnector
|
An initialized connector instance. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no connector is registered for the config type. |
Source code in src/fennflow/connectors/_factory.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
InMemoryConnector
Bases: AbstractConnector
In-memory connector for file storage, primarily used for testing.
Stores files in a class-level dictionary shared across all instances,
Use drop_all() between tests to reset state.
Example::
class UOW(UnitOfWork):
config = ConfigDict(
connector=InMemoryConnectorConfig(),
)
Source code in src/fennflow/connectors/in_memory/core.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
InMemoryConnectorConfig
Bases: AbstractConnectorConfig
Configuration for the in-memory connector.
No configuration is required — the in-memory connector is zero-dependency and is intended for testing and development only.
Source code in src/fennflow/connectors/in_memory/config.py
4 5 6 7 8 9 | |
S3Connector
Bases: AbstractConnector[S3Extra]
Connector for AWS S3-compatible object storage via aiobotocore.
Use S3ConnectorConfig to configure credentials, region, etc.
Example::
class UOW(UnitOfWork):
config = ConfigDict(
connector=S3ConnectorConfig(...),
)
Source code in src/fennflow/connectors/s3/core.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | |
S3ConnectorConfig
Bases: AbstractConnectorConfig
Configuration for the S3 connector.
Credentials can be provided explicitly via this config or through any method
supported by the AWS credential chain
(environment variables, ~/.aws/credentials, IAM roles, etc.).
See the AWS documentation
<https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html>_
for the full list of supported options.
Attributes:
| Name | Type | Description |
|---|---|---|
aws_access_key_id |
str | None
|
AWS access key ID. |
aws_secret_access_key |
str | None
|
AWS secret access key. |
endpoint_url |
str | None
|
Custom endpoint URL for S3-compatible storage. |
aiobotocore_config |
AioConfig | None
|
Advanced aiobotocore client configuration. |
Example::
# explicit credentials
class UOW(UnitOfWork):
user_files = UserFiles
config = ConfigDict(
connector=S3ConnectorConfig(
aws_access_key_id="key",
aws_secret_access_key="secret",
)
)
# rely on AWS credential chain
S3ConnectorConfig()
Source code in src/fennflow/connectors/s3/config.py
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
CreateRepository
Bases: AtRepository, ValidateDuplicatesMixin
Repository for uploading (creating) files in the storage.
This repository implements the "create" operation, which uploads new files to the configured storage (e.g. S3) within the current Unit of Work.
Behavior:
- Each file is registered in the backend as a pending operation
- Files are uploaded via the connector
- Backend commit is executed on uow.commit
Source code in src/fennflow/repositories/create.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
create(*files, connector_extra=OMIT)
async
Puts file if it doesn't exist in the backend.
Example::
file1 = TextContent.from_content("This is the first file.")
await uow.user_files.at("user1/").create(file1)
Raises:
| Type | Description |
|---|---|
RecordAlreadyExistsException
|
If a file with the same path already exists in a backend |
FilepathsCollisionError
|
If files with the same filepath are passed |
Source code in src/fennflow/repositories/create.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
DeletePrefixRepository
Bases: AtRepository
Repository mixin for deleting files from storage by prefix.
Implements Saga-based deletion with automatic compensation on failure.
Source code in src/fennflow/repositories/delete_prefix.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
delete_prefix(prefix, *, connector_extra=OMIT)
async
Delete files from storage by prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Prefix files relative to the current directory. |
required |
connector_extra
|
ConnectorExtra
|
Additional kwargs forwarded to the connector. |
OMIT
|
Returns:
| Type | Description |
|---|---|
DeleteResponse
|
DeleteResponse containing a result per path in the same order |
DeleteResponse
|
as it was listed. |
DeleteResponse
|
Each element is a ConnectorRawResponse. |
Example::
async with UOW() as uow:
await uow.user_files.delete_prefix("") # deletes all files in the bucket
Source code in src/fennflow/repositories/delete_prefix.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
DeleteRepository
Bases: AtRepository
Repository mixin for deleting files from storage.
Implements Saga-based deletion with automatic compensation on failure.
Source code in src/fennflow/repositories/delete.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
delete(*paths, connector_extra=OMIT)
async
Delete files from storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
str
|
Paths to the files relative to the current directory. |
()
|
connector_extra
|
ConnectorExtra
|
Additional kwargs forwarded to the connector. |
OMIT
|
Returns:
| Type | Description |
|---|---|
DeleteResponse
|
DeleteResponse containing a result per path in the same order as input. |
DeleteResponse
|
Each element is a ConnectorRawResponse if the file was deleted, |
DeleteResponse
|
or None if the path did not exist in the backend. |
Source code in src/fennflow/repositories/delete.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
GeneratePresignedUrlPrefixRepository
Bases: AtRepository
Repository mixin for generating presigned urls by prefix.
Combines ListRepository and GeneratePresignedUrlRepository.
Source code in src/fennflow/repositories/generate_presigned_url_prefix.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
generate_presigned_url_prefix(prefix, *, expires_in=OMIT, connector_extra=OMIT)
async
Generate a presigned URLs for the given prefix.
Does not interact with the backend or file storage. No Saga compensation is applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Prefix files relative to the current directory. |
required |
expires_in
|
Omittable[int]
|
Expiry duration in seconds. When omitted, the connector's default is used. |
OMIT
|
connector_extra
|
ConnectorExtra
|
Additional kwargs forwarded to the connector. |
OMIT
|
Returns:
| Type | Description |
|---|---|
PresignedUrlResponse
|
PresignedUrlResponse containing the generated URLs. |
Raises:
| Type | Description |
|---|---|
ConnectorCapabilityException
|
If the configured connector does not support presigned URL generation. |
Example::
async with UOW() as uow:
response = await uow.files.generate_presigned_url("user1/"
expires_in=600,
)
print(response.results) # list[str | None]
print(list(response.urls)) # list[str]
Source code in src/fennflow/repositories/generate_presigned_url_prefix.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
GeneratePresignedUrlRepository
Bases: AtRepository
Repository mixin for generating presigned URLs.
Provides access to connector-level presigned URL generation without touching the backend or file storage.
Note
This mixin is only supported by connectors that implement presigned URL generation (e.g. S3Connector). Using it with InMemoryConnector or LocalConnector will raise ConnectorCapabilityException.
Source code in src/fennflow/repositories/generate_presigned_url.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
generate_presigned_url(*storage_paths, expires_in=OMIT, connector_extra=OMIT)
async
Generate a presigned URL for the given path.
Does not interact with the backend or file storage. No Saga compensation is applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
storage_paths
|
str
|
Paths to the files relative to the current directory. |
()
|
expires_in
|
Omittable[int]
|
Expiry duration in seconds. When omitted, the connector's default is used. |
OMIT
|
connector_extra
|
Omittable[Any]
|
Additional connector-specific parameters passed directly to the connector. |
OMIT
|
Returns:
| Type | Description |
|---|---|
PresignedUrlResponse
|
PresignedUrlResponse containing the generated URLs. |
Raises:
| Type | Description |
|---|---|
ConnectorCapabilityException
|
If the configured connector does not support presigned URL generation. |
Example::
async with UOW() as uow:
response = await uow.files.at("user1/").generate_presigned_url(
"report.pdf",
"NonExistingFile",
expires_in=600,
)
print(response.results) # [<Link>, None]
print(list(response.urls)) # [<Link>]
Source code in src/fennflow/repositories/generate_presigned_url.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | |
GetPrefixRepository
Bases: AtRepository
Repository mixin for getting files from storage by prefix.
Combines ListRepository and GetRepository.
Source code in src/fennflow/repositories/get_prefix.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
get_prefix(prefix, *, connector_extra=OMIT)
async
Get files from storage by prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Prefix files relative to the current directory. |
required |
connector_extra
|
ConnectorExtra
|
Additional kwargs forwarded to the connector. |
OMIT
|
Returns:
| Type | Description |
|---|---|
MediaResponse[Any]
|
MediaResponse |
Example::
async with UOW() as uow:
await uow.user_files.get_prefix("some_prefix")
Source code in src/fennflow/repositories/get_prefix.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
GetRepository
Bases: AtRepository
Repository for retrieving a file from storage within the current scope.
This method returns a MediaResponse object containing the requested file,
if it exists according to the backend (source of truth).
Behavior:
- The backend is treated as the source of truth
- If the file is not present in the backend, the storage is NOT queried
- If the file exists in the backend, it is fetched from the storage via the connector
Notes:
- This method is read-only and does not participate in transaction flows (no saga)
- No network request is made if the backend does not contain the file
- Storage and backend may become inconsistent
(e.g. after restart with InMemoryBackend);
in such cases, use a reconcile mechanism to resync state.
(
)
Source code in src/fennflow/repositories/get.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
get(*paths, connector_extra=OMIT)
async
Retrieve a file from storage within the current scope.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*paths
|
str
|
Relative file's paths within the scoped repository |
()
|
connector_extra
|
ConnectorExtra
|
Additional connector-specific parameters passed directly
to the connector (e.g. S3 |
OMIT
|
Returns:
| Name | Type | Description |
|---|---|---|
MediaResponse |
MediaResponse[Any]
|
|
Example::
response = await uow.user_files.at("user1/").get("file.txt")
if response:
file = response[0]
Source code in src/fennflow/repositories/get.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
ListRepository
Bases: AtRepository
Repository for retrieving a files from storage within the current scope.
Source code in src/fennflow/repositories/list.py
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
list(prefix='', continuation_token=OMIT, limit=1000)
async
Uploads files under the current path, optionally filtered by prefix.
Files are visible if they are uploaded (committed) or pending within the current session. Pending files from other sessions are not returned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Sub-path to filter results. Appended to the current |
''
|
continuation_token
|
Omittable[str]
|
Opaque token returned by a previous call to continue paginating. |
OMIT
|
limit
|
int
|
Maximum number of storage_paths to return. Defaults to |
1000
|
Returns:
| Name | Type | Description |
|---|---|---|
ListResponse |
ListResponse
|
A container of storage_paths matching the query. Includes
a |
Example::
async with UOW() as uow:
await uow.files.at("folder1/").put(file1, file2, file3)
page = await uow.files.at("folder1/").list(limit=2)
next_page = await uow.files.at("folder1/").list(
limit=2,
continuation_token=page.continuation_token,
)
Source code in src/fennflow/repositories/list.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
PutRepository
Bases: AtRepository, ValidateDuplicatesMixin
Repository for upserting files in the storage.
This repository implements the "put" operation, which uploads new files to the configured storage (e.g. S3) within the current Unit of Work.
Behavior:
- Each file is registered in the backend as a pending operation
- Files are uploaded via the connector
- Backend commit is executed on uow.commit
Source code in src/fennflow/repositories/put.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
put(*files, connector_extra=OMIT)
async
Puts file into storage.
Example::
file1 = TextContent.from_content("This is the first file.")
await uow.user_files.at("user1/").put(file1)
Raises:
| Type | Description |
|---|---|
FilepathsCollisionError
|
If files with the same filepath are passed |
Source code in src/fennflow/repositories/put.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
RepoField
Bases: Generic[RepoType]
A descriptor that lazily initializes a repository instance on a UnitOfWork.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_cls
|
type[RepoType]
|
The repository class to instantiate. |
required |
namespace
|
Namespace
|
The storage namespace (e.g. S3 bucket name) for this repository. |
required |
Example::
class UOW(UnitOfWork):
user_files = RepoField(UserFiles, namespace="user-files")
Source code in src/fennflow/repositories/fields/base.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
S3RepoField(repo_cls, bucket_name)
Create a RepoField configured for S3 storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_cls
|
type[RepoType]
|
The repository class to instantiate. |
required |
bucket_name
|
BucketName
|
alias for RepoField.namespace. |
required |
Returns:
| Type | Description |
|---|---|
RepoField[RepoType]
|
A configured RepoField bound to the given repository class. |
Example::
class UOW(UnitOfWork):
user_files = S3RepoField(UserFiles, bucket_name="my-bucket")
Source code in src/fennflow/repositories/fields/s3.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
AtRepository
Bases: BaseRepository
Repository mixin that adds path navigation capabilities.
Allows scoping operations to a specific path within the namespace
using the at() method.
Source code in src/fennflow/repositories/at.py
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
cwd
property
Return the current working path.
Returns:
| Type | Description |
|---|---|
str
|
The normalized current path string. |
at(path)
Return a new repository instance scoped to the given path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | PathTemplate
|
Path to scope the repository to, relative to the current path. |
required |
Returns:
| Type | Description |
|---|---|
Self
|
A new repository instance with the updated path. |
Example::
storage = uow.user_files.at("user1/")
print(storage.cwd) # "user1/"
Example::
@dataclass(slots=True)
class YearMonth:
year: int
month: int
def render(self) -> str:
return f"{self.year}/{self.month:02d}"
storage = uow.user_files.at("user1/").at(YearMonth(year=2026, month=1))
print(storage.cwd) # user1/2026/01/
Source code in src/fennflow/repositories/at.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
AudioContent
Bases: BaseBinary
Media content representing an audio file.
Attributes:
| Name | Type | Description |
|---|---|---|
duration |
int | None
|
Duration of the audio in seconds, if known. |
Source code in src/fennflow/files/media/audio_content.py
4 5 6 7 8 9 10 11 | |
BaseBinary
Bases: BaseContent
Base class for binary content types.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
bytes
|
raw file's content |
Source code in src/fennflow/files/media/base_binary.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
BinaryContent
Bases: BaseBinary
Class for arbitrary binary data.
Source code in src/fennflow/files/media/binary_content.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
from_local_path(path, media_type=OMIT, **kwargs)
classmethod
Create a BinaryContent instance from the local filesystem.
The MIME type is guessed from the file extension when media_type is not
provided. The filename is taken from the final component of path and can
be overridden via kwargs.
Unlike ContentFactory.from_local_path, this method always returns an
instance of the class it is called on — it never resolves a richer subtype
from the content registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Absolute or relative path to the file, as a |
required |
media_type
|
Omittable[MediaTypes]
|
MIME type to assign to the content. When omitted, the type
is guessed from the file extension via |
OMIT
|
**kwargs
|
Any
|
Additional fields forwarded to the content model (e.g.
|
{}
|
Returns:
| Type | Description |
|---|---|
Self
|
An instance of |
Self
|
on) containing the file's raw bytes and resolved metadata. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If |
ExtensionCannotBeGuessed
|
If |
Example::
from fennflow.files import MediaType
from fennflow.files import BinaryContent
# MIME type guessed from extension
content = BinaryContent.from_local_path("report.pdf")
# Explicit MIME type
content = BinaryContent.from_local_path(
"dump.bin",
media_type=MediaType.APPLICATION_OCTET_STREAM,
)
# Override filename and attach extra metadata
content = BinaryContent.from_local_path(
"/tmp/upload_xyz",
media_type=MediaType.IMAGE_PNG,
filename="avatar.png",
uploaded_by="alice",
)
Notes
This method is not inherited by other binary content classes such as TextContent, ImageContent, etc. Because this method returns an instance of the class it is called on, inheritance would allow incorrect usage — for example, obtaining an ImageContent instance from "text.txt". To get a specific content type, use ContentFactory.from_local_path or the class constructor directly (e.g. ImageContent(...)).
Source code in src/fennflow/files/media/binary_content.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
ContentFactory
Factory for creating media content instances from raw data.
Resolves the appropriate content class from the registry based on
MIME type, falling back to BaseBinary for unknown types.
Source code in src/fennflow/files/factory.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
from_bytes(media_type, data, **kwargs)
classmethod
Create a media content instance from raw bytes.
Resolves the content class from the registry by exact MIME type match,
then by prefix match, falling back to BaseBinary if no match is found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
media_type
|
MediaTypes
|
The MIME type of the content (e.g. |
required |
data
|
bytes
|
The raw bytes to wrap. |
required |
**kwargs
|
Any
|
Additional fields passed to the content model. |
{}
|
Returns:
| Type | Description |
|---|---|
BinaryMedia
|
A media content instance appropriate for the given MIME type. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the resolved content class fails validation. |
Example:
from fennflow.files import MediaType
content = ContentFactory.from_bytes(MediaType.TEXT_PLAIN, b"Hello, World!")
Source code in src/fennflow/files/factory.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | |
from_url(url, media_type=MediaType.APPLICATION_OCTET_STREAM, **kwargs)
staticmethod
Create a UrlContent instance from a URL string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
The URL string to wrap. |
required |
media_type
|
MediaTypes
|
The MIME type of the resource.
Defaults to |
APPLICATION_OCTET_STREAM
|
**kwargs
|
Any
|
Additional fields passed to the content model. |
{}
|
Returns:
| Type | Description |
|---|---|
UrlContent
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the resolved content class fails validation. |
Example::
url = ContentFactory.from_url("https://example.com/file.txt")
Source code in src/fennflow/files/factory.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
DocumentContent
Bases: BaseBinary
Media content representing a document.
Source code in src/fennflow/files/media/document_content.py
4 5 | |
ImageContent
Bases: BaseBinary
Media content representing an image file.
Attributes:
| Name | Type | Description |
|---|---|---|
height |
int | None
|
Height of the image in pixels, if known. |
width |
int | None
|
Width of the image in pixels, if known. |
Source code in src/fennflow/files/media/image_content.py
4 5 6 7 8 9 10 11 12 13 | |
JsonContent
Bases: BaseBinary, FromContentAbstract, ContentPropertyAbstract
Media content representing a JSON file.
Stores JSON data as UTF-8 encoded bytes internally.
Use from_content() to create from a Python object.
Attributes:
| Name | Type | Description |
|---|---|---|
encoding |
str
|
The text encoding. Defaults to |
Example::
file = JsonContent.from_content({"key": "value"})
print(file.content) # {"key": "value"}
await uow.user_files.at("user1/").put(file)
Source code in src/fennflow/files/media/json_content.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
MediaType
Bases: StrEnum
Common MIME types for use with FennFlow content models.
Source code in src/fennflow/files/enums.py
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
TextContent
Bases: BaseBinary, FromContentAbstract, ContentPropertyAbstract
Media content representing a plain text file.
Stores text as UTF-8 encoded bytes internally.
Use from_content() to create from a string.
Attributes:
| Name | Type | Description |
|---|---|---|
encoding |
str
|
The text encoding. Defaults to |
Example::
file = TextContent.from_content("Hello, World!")
print(file.content) # "Hello, World!"
await uow.user_files.at("user1/").put(file)
Source code in src/fennflow/files/media/text_content.py
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
UrlContent
Bases: BaseContent
Media content representing a URL.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
str
|
The URL string. |
Source code in src/fennflow/files/media/url_content.py
4 5 6 7 8 9 10 11 | |
VideoContent
Bases: BaseBinary
Media content representing a video file.
Attributes:
| Name | Type | Description |
|---|---|---|
duration |
int | None
|
Duration of the video in seconds. |
height |
int | None
|
Height of the video in pixels. |
width |
int | None
|
Width of the video in pixels. |
Source code in src/fennflow/files/media/video_content.py
4 5 6 7 8 9 10 11 12 13 14 15 | |
ReconcileConfig
Bases: BasePydanticConfig
Configuration for the reconciler.
Source code in src/fennflow/reconciler/config.py
7 8 9 10 11 12 13 14 15 16 | |
ReconcileFrequencyEnum
Bases: StrEnum
Controls how often reconciliation is performed.
Attributes:
| Name | Type | Description |
|---|---|---|
ON_START_APP |
Reconcile once per process lifetime. |
|
ON_SESSION_START |
Reconcile on every |
|
NEVER |
Disable reconciliation. |
Source code in src/fennflow/reconciler/enums.py
4 5 6 7 8 9 10 11 12 13 14 15 | |
ReconcileStrategyEnum
Bases: StrEnum
Defines how reconciliation updates existing data.
Attributes:
| Name | Type | Description |
|---|---|---|
FILL_IF_EMPTY |
Reconcile only if the backend is empty. |
|
REPLACE |
Reset all backend data before reconciling. |
|
INSERT_MISSING |
Insert missing data while keeping existing data intact. |
Source code in src/fennflow/reconciler/enums.py
18 19 20 21 22 23 24 25 26 27 28 29 | |
Reconciler
Synchronizes backend state with actual connector (storage) state.
On startup or session start, the backend may be out of sync with the
real storage (e.g. on first connection with a persistent backend).
Reconciler restores consistency
by listing files from the connector and inserting them into the backend
according to the chosen strategy.
Called internally by ReconcileOrchestrator in UnitOfWork.aenter.
Notes
Reconciler does not perform garbage collection!
Example::
import asyncio
from fennflow.reconciler import Reconciler, ReconcileStrategyEnum
from fennflow.uow import UowInspector
async def main():
async with UOW() as uow:
uow_inspector = UowInspector(uow=uow)
reconcile = Reconciler(
uow_fields=uow_inspector.get_repo_fields(),
connector=uow.connector,
backend=uow.backend,
)
await reconcile.reconcile(
session_id=uow._session_id,
batch_size=500,
strategy=ReconcileStrategyEnum.REPLACE,
backend_scope=uow.config["connector"].scope
)
if __name__ == "__main__":
asyncio.run(main())
Source code in src/fennflow/reconciler/core.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
__init__(uow_fields, backend, connector)
Init method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uow_fields
|
Iterable[RepoField]
|
Repository field descriptors to reconcile. Each field provides the namespace and repo config needed to list objects from the connector. |
required |
backend
|
BackendOrchestrator
|
The backend to sync state into. |
required |
connector
|
AbstractConnector
|
The storage connector to read the source-of-truth from. |
required |
Source code in src/fennflow/reconciler/core.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
reconcile(session_id, strategy, batch_size, backend_scope)
async
Reconcile all registered repository fields against the connector.
Iterates over each RepoField, lists its objects from the connector
in pages, and inserts them into the backend. The conflict resolution
behavior is determined by strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
UUID
|
Session ID to stamp on inserted |
required |
strategy
|
ReconcileStrategyEnum
|
Controls whether to skip reconciliation, overwrite existing
records, or only insert missing ones. See |
required |
batch_size
|
int
|
Number of objects to fetch per page from the connector. |
required |
backend_scope
|
BackendScope
|
Scope to assign to inserted records in the backend. |
required |
Raises:
| Type | Description |
|---|---|
ReconcileFailedException
|
If any error occurs during reconciliation. |
Source code in src/fennflow/reconciler/core.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |