Skip to content

Commit 0573271

Browse files
authored
Merge pull request #214 from OpenSemanticLab/feat/file-controllers-from-core
refactor: take the file controllers from opensemantic.core
2 parents fa07163 + 8f7cc51 commit 0573271

11 files changed

Lines changed: 402 additions & 162 deletions

File tree

‎pyproject.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ classifiers = [
2121
"Topic :: Software Development :: Libraries :: Python Modules",
2222
]
2323
dependencies = [
24-
"oold>=0.15.0",
24+
# registered_credentials(), used to find the wiki of a file without a URL
25+
"oold>=2.3.0",
2526
"opensemantic",
26-
"opensemantic.core>=0.57.4",
27+
# the file controllers this module wraps
28+
"opensemantic.core>=0.59.0",
2729
"opensemantic.base>=0.42.7",
2830
"pydantic[email]>=2.12.0",
2931
"datamodel-code-generator==0.51.0",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""File controllers.
2+
3+
Imported eagerly so the controllers register themselves: a caller that resolves
4+
a file entity through the controller registry then finds the wiki
5+
implementation without importing it by name.
6+
"""
7+
8+
from osw.controller.file import base, local, memory, remote, wiki
9+
10+
__all__ = ["base", "local", "memory", "remote", "wiki"]

‎src/osw/controller/file/base.py‎

Lines changed: 6 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,9 @@
1-
from abc import abstractmethod
2-
from typing import IO, Any, Dict
1+
"""File controller interface.
32
4-
from osw.core import model
3+
The implementation moved to ``opensemantic.core``, which owns the file models.
4+
This module keeps the import path working.
5+
"""
56

7+
from opensemantic.core.v1 import FileControllerMixin as FileController
68

7-
class FileController(model.File):
8-
"""Base class for file controllers"""
9-
10-
@abstractmethod
11-
def get(self) -> IO:
12-
"""Reads or downloads a file from an IO object
13-
Make sure to close the IO object after reading
14-
15-
Returns
16-
-------
17-
the IO object of the opened file
18-
"""
19-
20-
@abstractmethod
21-
def put(self, file: IO, **kwargs: Dict[str, Any]):
22-
"""Writes or uploads a file from an IO object
23-
24-
Parameters
25-
-------
26-
file
27-
the IO object to read from
28-
"""
29-
30-
def put_from(self, other: "FileController", **kwargs: Dict[str, Any]):
31-
"""Writes or uploads a file from another file controller
32-
Automatically closes the other file controllers IO object after reading
33-
34-
Parameters
35-
----------
36-
other
37-
another file controller to read from
38-
"""
39-
with other.get() as f:
40-
self.put(f, **kwargs)
41-
42-
def get_to(self, other: "FileController"):
43-
"""Reads or downloads a file to another file controller
44-
Automatically closes this file controllers IO object after reading
45-
46-
Parameters
47-
----------
48-
other
49-
another file controller to write to
50-
"""
51-
with self.get() as f:
52-
other.put(f)
53-
54-
@classmethod
55-
def from_other(cls, other: "FileController", **kwargs: Any) -> "FileController":
56-
"""Create an instance based on another file controller
57-
copying all common attributes
58-
59-
Parameters
60-
----------
61-
other
62-
another file controller to copy from
63-
kwargs
64-
additional attributes to be set
65-
"""
66-
return other.cast(cls, **kwargs)
9+
__all__ = ["FileController"]

‎src/osw/controller/file/local.py‎

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,68 @@
1+
"""Local file controller.
2+
3+
The implementation moved to ``opensemantic.core``, which owns the file models.
4+
This subclass keeps the ``path`` argument and the filename-derived label of the
5+
controller it replaces.
6+
"""
7+
18
import os
2-
import shutil
3-
from io import TextIOBase
49
from pathlib import Path
5-
from typing import IO, Any, Dict, List, Optional
10+
from typing import IO, Any, Dict, Optional
11+
12+
from opensemantic.core.v1 import LocalFileController as _LocalFileController
613

7-
from osw.controller.file.base import FileController
814
from osw.core import model
915

1016

11-
class LocalFileController(FileController, model.LocalFile):
17+
class LocalFileController(_LocalFileController):
1218
"""File controller for local files"""
1319

14-
label: Optional[List[model.Label]] = [model.Label(text="Unnamed file")]
15-
"""The label of the file, defaults to 'Unnamed file'"""
16-
path: Path
17-
"""The path to the file"""
20+
path: Optional[Path] = None
21+
"""Where the content lives, as a Path. A field, not a property: a property
22+
of this name would win over the ``path`` field declared by
23+
``osw.express.FileResult`` and make that field's value unreadable. Kept in
24+
step with the model's own ``local_path`` by the constructor, so either name
25+
reads the same file."""
1826

1927
class Config:
2028
arbitrary_types_allowed = True
2129

22-
@classmethod
23-
def from_other(
24-
cls, other: "FileController", path: Path, **kwargs
25-
) -> "LocalFileController":
26-
return other.cast(cls, path=path, **kwargs)
27-
2830
def __init__(self, **kwargs):
31+
path = kwargs.get("path")
32+
if path is not None and not kwargs.get("local_path"):
33+
kwargs["local_path"] = str(path)
2934
super().__init__(**kwargs)
35+
if not self.local_path:
36+
# The controller this replaces declared path as a required field,
37+
# so an instance without one never existed. Raising here keeps the
38+
# error at the call site that is wrong, instead of surfacing it on
39+
# the first read.
40+
raise ValueError(
41+
f"{type(self).__name__} needs a file: pass path= or local_path="
42+
)
43+
self.path = Path(self.local_path)
3044
self._set_metadata()
3145

46+
@classmethod
47+
def from_other(cls, other, path=None, **kwargs) -> "LocalFileController":
48+
if path is not None:
49+
kwargs["local_path"] = str(path)
50+
return other.cast(cls, **kwargs)
51+
3252
def get(self) -> IO:
3353
self._set_metadata()
34-
return open(self.path, "rb")
54+
return super().get()
3555

3656
def put(self, file: IO, **kwargs: Dict[str, Any]):
3757
self._set_metadata()
38-
mode = "wb"
39-
if isinstance(file, TextIOBase):
40-
mode = "w"
41-
with open(self.path, mode) as f:
42-
# f.write(file.read()) # In-memory - limited by available RAM
43-
shutil.copyfileobj(file, f) # Chunked copy
44-
45-
def delete(self):
46-
if os.path.exists(self.path):
47-
os.remove(self.path)
58+
super().put(file, **kwargs)
4859

4960
def _set_metadata(self):
50-
file = LocalFileController.extract_metadata(self.path)
51-
self.label = file.label
61+
self.label = LocalFileController.extract_metadata(self.path).label
5262

5363
@classmethod
5464
def extract_metadata(cls, path: Path) -> model.LocalFile:
55-
file = model.LocalFile(label=[model.Label(text=path.name)])
65+
file = model.LocalFile(label=[model.Label(text=Path(path).name)])
5666
if os.path.exists(path):
5767
pass
5868
# stats = os.stat(path)

‎src/osw/controller/file/memory.py‎

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,9 @@
1-
import shutil
2-
from io import BytesIO
3-
from typing import IO, Any, Dict, List, Optional
1+
"""In-memory file controller.
42
5-
from pydantic.v1 import Field
3+
The implementation moved to ``opensemantic.core``, which owns the file models.
4+
This module keeps the import path working.
5+
"""
66

7-
from osw.controller.file.base import FileController
8-
from osw.core import model
7+
from opensemantic.core.v1 import InMemoryController
98

10-
11-
class InMemoryController(FileController, model.LocalFile):
12-
"""File controller for in-memory streams"""
13-
14-
label: Optional[List[model.Label]] = [model.Label(text="Unnamed stream")]
15-
"""the label of the stream, e.g., the name of the file the stream
16-
originates from. Defaults to 'Unnamed stream'."""
17-
stream: Any = Field(default_factory=BytesIO)
18-
"""the stream to the file, any file-like object. Defaults to an empty
19-
binary buffer. Byte-oriented, to match the get/put counterparts."""
20-
21-
class Config:
22-
arbitrary_types_allowed = True
23-
24-
def get(self) -> IO:
25-
return self.stream
26-
27-
def put(self, file: IO, **kwargs: Dict[str, Any]):
28-
shutil.copyfileobj(file, self.stream)
9+
__all__ = ["InMemoryController"]

‎src/osw/controller/file/remote.py‎

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,9 @@
1-
from abc import abstractmethod
2-
from typing import IO, Any, Dict
1+
"""Remote file controller.
32
4-
from osw.controller.file.base import FileController
5-
from osw.core import model
3+
The implementation moved to ``opensemantic.core``, which owns the file models.
4+
This module keeps the import path working.
5+
"""
66

7-
# TODO: add additional remove file with
8-
# https://docs.prefect.io/2.11.4/concepts/filesystems/
7+
from opensemantic.core.v1 import RemoteFileController
98

10-
11-
# Note: the order of the base classes is important
12-
# The data class must be the first base class, otherwise subclass controllers fall back
13-
# to the data model of the controller superclass
14-
class RemoteFileController(model.RemoteFile, FileController):
15-
@abstractmethod
16-
def get(self) -> IO:
17-
pass
18-
19-
@abstractmethod
20-
def put(self, file: IO, **kwargs: Dict[str, Any]):
21-
pass
22-
23-
# @classmethod
24-
# def from_local(self, local: "local.LocalFileController") -> "RemoteFileController":
25-
# rf = local.cast(self)
26-
# rf.put(local.get())
27-
# return rf
28-
29-
# def from_local(self, local: "local.LocalFileController") -> None:
30-
# with local.get() as f:
31-
# self.put(f)
9+
__all__ = ["RemoteFileController"]

0 commit comments

Comments
 (0)