Skip to content

Commit 58939eb

Browse files
committed
feat: global storage configuration via file-keeper.json
1 parent 84136d0 commit 58939eb

21 files changed

Lines changed: 114 additions & 1 deletion

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ test = [ "pytest-cov", "pytest-faker", "responses", "werkzeug", "faker" ]
3939
docs = [ "mkdocs", "mkdocs-material", "pymdown-extensions", "mkdocstrings[python]", ]
4040
dev = [ "pytest-cov", "pytest-faker", "responses", "mkdocs", "mkdocs-material", "pymdown-extensions", "mkdocstrings[python]",]
4141

42+
user_config = ["platformdirs"]
43+
4244
azure = ["azure-storage-blob",]
4345
gcs = [ "google-cloud-storage",]
4446
libcloud = [ "apache-libcloud", "cryptography",]

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Package definition."""
2+
23
from setuptools import setup
34

45
setup()

src/file_keeper/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Main entrypoint of the file-keeper."""
2+
23
__version__ = "0.1.0a0"
34

45
from .core import exceptions as exc
@@ -12,6 +13,7 @@
1213
Storage,
1314
Uploader,
1415
adapters,
16+
get_storage,
1517
make_storage,
1618
)
1719
from .core.types import Location, SignedAction
@@ -45,6 +47,7 @@
4547
"hookimpl",
4648
"humanize_filesize",
4749
"make_storage",
50+
"get_storage",
4851
"make_upload",
4952
"parse_filesize",
5053
"types",

src/file_keeper/core/data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
@dataclasses.dataclass(frozen=True)
2828
class BaseData(Generic[TData]):
2929
"""Base class for file details."""
30+
3031
location: types.Location
3132
size: int = 0
3233
content_type: str = ""

src/file_keeper/core/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Registry for collections."""
2+
23
from __future__ import annotations
34

45
from collections.abc import Callable, Hashable, Mapping, MutableMapping
@@ -78,6 +79,7 @@ def pop(self, key: K) -> V | None:
7879

7980
def decorated(self, key: K):
8081
"""Collect member via decorator."""
82+
8183
def decorator(value: V):
8284
self.register(key, value)
8385
return value

src/file_keeper/core/storage.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,32 @@
1414
import dataclasses
1515
import functools
1616
import inspect
17+
import json
1718
import logging
1819
import os
20+
import pathlib
1921
from abc import ABC
2022
from collections.abc import Callable, Iterable, Mapping
21-
from typing import Any, ClassVar, TypeAlias, cast
23+
from typing import Any, ClassVar, Literal, TypeAlias, cast
2224

2325
from typing_extensions import ParamSpec, TypeVar, override
2426

2527
from . import data, exceptions, types, utils
2628
from .registry import Registry
2729
from .upload import Upload, make_upload
2830

31+
try:
32+
from platformdirs import user_config_dir # pyright: ignore[reportAssignmentType]
33+
except ImportError:
34+
35+
def user_config_dir(
36+
appname: str | None = None,
37+
appauthor: str | Literal[False] | None = None,
38+
):
39+
"""Mock for user config locator."""
40+
return
41+
42+
2943
P = ParamSpec("P")
3044
T = TypeVar("T")
3145
S = TypeVar("S", bound="Storage")
@@ -36,6 +50,8 @@
3650
Capability: TypeAlias = utils.Capability
3751

3852
adapters = Registry["type[Storage]"]()
53+
storages = Registry["Storage"]()
54+
3955
location_transformers = Registry[types.LocationTransformer]()
4056

4157

@@ -995,3 +1011,42 @@ def make_storage(name: str, settings: dict[str, Any]) -> Storage:
9951011
settings.setdefault("name", name)
9961012

9971013
return adapter(settings)
1014+
1015+
1016+
def get_storage(name: str, settings: dict[str, Any] | None = None) -> Storage:
1017+
"""Get storage from the pool.
1018+
1019+
If storage accessed for the first time, it's initialized and added to the
1020+
pool. After that the same storage is returned every time the function is
1021+
called with the given name.
1022+
1023+
"""
1024+
if name not in storages:
1025+
if settings is None:
1026+
config_file = os.getenv("FILE_KEEPER_CONFIG")
1027+
1028+
if not config_file and (config_dir := user_config_dir("file-keeper")):
1029+
config_file = os.path.join(config_dir, "file-keeper.json")
1030+
1031+
if not config_file or not os.path.isfile(config_file):
1032+
path = pathlib.Path().absolute()
1033+
1034+
while len(path.parts) > 1:
1035+
config_file = str(path / "file-keeper.json")
1036+
if os.path.exists(config_file):
1037+
break
1038+
path = path.parent
1039+
else:
1040+
config_file = None
1041+
1042+
if config_file:
1043+
log.debug("Load configuration from %s", config_file)
1044+
with open(config_file) as src:
1045+
settings = json.load(src).get("storages", {}).get(name)
1046+
1047+
if not settings:
1048+
raise exceptions.UnknownStorageError(name)
1049+
1050+
storages.register(name, make_storage(name, settings))
1051+
1052+
return storages[name]

src/file_keeper/core/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Type definitions."""
2+
23
from __future__ import annotations
34

45
from collections.abc import Callable, Iterator
@@ -17,16 +18,19 @@
1718

1819
class PReadable(Protocol):
1920
"""Readable object."""
21+
2022
def read(self, size: Any = ..., /) -> bytes: ...
2123

2224

2325
class PStream(PReadable, Protocol):
2426
"""Readable stream."""
27+
2528
def __iter__(self) -> Iterator[bytes]: ...
2629

2730

2831
class PSeekableStream(PStream, Protocol):
2932
"""Stream that supports `seek` operation."""
33+
3034
def tell(self) -> int:
3135
"""Get the current position of the pointer."""
3236
...
@@ -38,6 +42,7 @@ def seek(self, offset: int, whence: int = 0) -> int:
3842

3943
class PData(Protocol):
4044
"""Structure of the *Data object."""
45+
4146
location: Location
4247
size: int
4348
content_type: str

src/file_keeper/core/upload.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Upload implementation."""
2+
23
from __future__ import annotations
34

45
import dataclasses

src/file_keeper/core/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def humanize_filesize(value: int | float, base: int = SI_BASE) -> str:
277277

278278
class AbstractReader(Generic[T], abc.ABC):
279279
"""Abstract wrapper that transforms data into readable stream."""
280+
280281
source: T
281282
chunk_size: int
282283

@@ -296,6 +297,7 @@ def read(self, size: int | None = None) -> bytes:
296297

297298
class IterableBytesReader(AbstractReader[Iterable[int]]):
298299
"""Wrapper that transforms iterable of bytes into readable stream."""
300+
299301
def __init__(self, source: Iterable[bytes], chunk_size: int = CHUNK_SIZE):
300302
super().__init__(itertools.chain.from_iterable(source), chunk_size)
301303

src/file_keeper/default/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Default implementations of file-keeper units."""
2+
23
from __future__ import annotations
34

45
import contextlib

0 commit comments

Comments
 (0)