Skip to content

Commit

Permalink
Simpler interface
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon committed Sep 12, 2024
1 parent 8881efa commit ce600f6
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 299 deletions.
17 changes: 16 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ fastjsonschema = ">=2.19.1"
moto = ">=5.0.14"
pytest-benchmark = ">=4.0.0"
pytest-snapshot = ">=0.9.0"
pytest-subtests = ">=0.13.1"
pytz = ">=2022.2.1"
requests-mock = ">=1.10.0"
rfc3339-validator = ">=0.1.4"
Expand Down
121 changes: 88 additions & 33 deletions singer_sdk/contrib/filesystem/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,121 @@
from __future__ import annotations

import abc
import enum
import io
import typing as t

if t.TYPE_CHECKING:
import datetime
import types

__all__ = ["AbstractDirectory", "AbstractFile", "AbstractFileSystem"]
__all__ = ["AbstractFile", "AbstractFileSystem"]

_F = t.TypeVar("_F")


class FileMode(str, enum.Enum):
read = "rb"
write = "wb"


class AbstractFile(abc.ABC):
"""Abstract class for file operations."""

def read_text(self, *, encoding: str = "utf-8") -> str:
"""Read the entire file as text.
def __init__(self, buffer: io.BytesIO, filename: str):
"""Create a new AbstractFile instance."""
self.buffer = buffer
self.filename = filename

def read(self, size: int = -1) -> bytes:
"""Read the file contents.
Args:
encoding: The text encoding to use.
size: The number of bytes to read. If -1, read the entire file.
Returns:
The file contents as a string.
The file contents as bytes.
"""
return self.read().decode(encoding)
return self.buffer.read(size)

@abc.abstractmethod
def read(self, size: int = -1) -> bytes:
"""Read the file contents."""
def write(self, data: bytes) -> int:
"""Write data to the file.
@property
def creation_time(self) -> datetime.datetime:
"""Get the creation time of the file."""
raise NotImplementedError
Args:
data: The data to write.
@property
def modified_time(self) -> datetime.datetime:
"""Get the last modified time of the file."""
raise NotImplementedError
Returns:
The number of bytes written.
"""
return self.buffer.write(data)

def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
"""Seek to a position in the file.
_F = t.TypeVar("_F")
_D = t.TypeVar("_D")
Args:
offset: The offset to seek to.
whence: The reference point for the offset.
Returns:
The new position in the file.
"""
return self.buffer.seek(offset, whence)

Check warning on line 64 in singer_sdk/contrib/filesystem/base.py

View check run for this annotation

Codecov / codecov/patch

singer_sdk/contrib/filesystem/base.py#L64

Added line #L64 was not covered by tests

class AbstractDirectory(abc.ABC, t.Generic[_F]):
"""Abstract class for directory operations."""
def tell(self) -> int:
"""Get the current position in the file.
@abc.abstractmethod
def list_contents(self: _D) -> t.Generator[_F | _D, None, None]:
"""List files in the directory.
Returns:
The current position in the file.
"""
return self.buffer.tell()

Check warning on line 72 in singer_sdk/contrib/filesystem/base.py

View check run for this annotation

Codecov / codecov/patch

singer_sdk/contrib/filesystem/base.py#L72

Added line #L72 was not covered by tests

def __enter__(self: _F) -> _F:
"""Enter the context manager.
Yields:
A file or directory node
Returns:
The file object.
"""
yield self
yield from []
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: types.TracebackType | None,
) -> None:
"""Close the file.
class AbstractFileSystem(abc.ABC, t.Generic[_F, _D]):
Args:
exc_type: The exception type.
exc_value: The exception value.
traceback: The traceback.
"""
self.close()

@abc.abstractmethod
def close(self) -> None:
"""Close the file."""

@abc.abstractmethod
def __iter__(self) -> t.Iterator[str]:
"""Iterate over the file contents as lines."""

@abc.abstractmethod
def seekable(self) -> bool:
"""Whether the file is seekable."""


class AbstractFileSystem(abc.ABC, t.Generic[_F]):
"""Abstract class for file system operations."""

@property
@abc.abstractmethod
def root(self) -> _D:
"""Get the root path."""
raise NotImplementedError
def open(self, filename: str, mode: str, newline: str, encoding: str) -> _F:
"""Open a file."""

@abc.abstractmethod
def modified(self, filename: str) -> datetime.datetime:
"""Get the last modified time of a file."""

@abc.abstractmethod
def created(self, filename: str) -> datetime.datetime:
"""Get the creation time of a file."""
115 changes: 35 additions & 80 deletions singer_sdk/contrib/filesystem/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,99 +8,54 @@

from singer_sdk.contrib.filesystem import base

__all__ = ["LocalDirectory", "LocalFile", "LocalFileSystem"]
if t.TYPE_CHECKING:
import contextlib

__all__ = ["LocalFile", "LocalFileSystem"]


class LocalFile(base.AbstractFile):
"""Local file operations."""

def __init__(self, filepath: str | Path):
"""Create a new LocalFile instance."""
self._filepath = filepath
self.path = Path(self._filepath).absolute()

def __repr__(self) -> str:
"""A string representation of the LocalFile.
Returns:
A string representation of the LocalFile.
"""
return f"LocalFile({self._filepath})"

def read(self, size: int = -1) -> bytes:
"""Read the file contents.
def close(self) -> None:
"""Close the file."""
return self.buffer.close()

Args:
size: Number of bytes to read. If not specified, the entire file is read.
Returns:
The file contents as a string.
"""
with self.path.open("rb") as file:
return file.read(size)

@property
def creation_time(self) -> datetime:
"""Get the creation time of the file.
def seekable(self) -> bool: # noqa: D102, PLR6301
return True

Check warning on line 25 in singer_sdk/contrib/filesystem/local.py

View check run for this annotation

Codecov / codecov/patch

singer_sdk/contrib/filesystem/local.py#L25

Added line #L25 was not covered by tests

Returns:
The creation time of the file.
"""
stat = self.path.stat()
try:
return datetime.fromtimestamp(stat.st_birthtime).astimezone() # type: ignore[attr-defined]
except AttributeError:
return datetime.fromtimestamp(stat.st_ctime).astimezone()

@property
def modified_time(self) -> datetime:
"""Get the last modified time of the file.
def __iter__(self) -> contextlib.Iterator[str]: # noqa: D105
return iter(self.buffer)

Check warning on line 28 in singer_sdk/contrib/filesystem/local.py

View check run for this annotation

Codecov / codecov/patch

singer_sdk/contrib/filesystem/local.py#L28

Added line #L28 was not covered by tests

Returns:
The last modified time of the file.
"""
return datetime.fromtimestamp(self.path.stat().st_mtime).astimezone()


class LocalDirectory(base.AbstractDirectory[LocalFile]):
"""Local directory operations."""

def __init__(self, dirpath: str | Path):
"""Create a new LocalDirectory instance."""
self._dirpath = dirpath
self.path = Path(self._dirpath).absolute()

def __repr__(self) -> str:
"""A string representation of the LocalDirectory.
Returns:
A string representation of the LocalDirectory.
"""
return f"LocalDirectory({self._dirpath})"

def list_contents(self) -> t.Generator[LocalFile | LocalDirectory, None, None]:
"""List files in the directory.
Yields:
A file or directory node
"""
for child in self.path.iterdir():
if child.is_dir():
subdir = LocalDirectory(child)
yield subdir
yield from subdir.list_contents()
else:
yield LocalFile(child)


class LocalFileSystem(base.AbstractFileSystem[LocalFile, LocalDirectory]):
class LocalFileSystem(base.AbstractFileSystem[LocalFile]):
"""Local filesystem operations."""

def __init__(self, root: str) -> None:
"""Create a new LocalFileSystem instance."""
self._root_dir = LocalDirectory(root)
self._root_path = Path(root).absolute()

@property
def root(self) -> LocalDirectory:
def root(self) -> Path:
"""Get the root path."""
return self._root_dir
return self._root_path

def open( # noqa: D102
self,
filename: str,
*,
mode: base.FileMode = base.FileMode.read,
) -> LocalFile:
filepath = self.root / filename
return LocalFile(filepath.open(mode=mode), filename)

def modified(self, filename: str) -> datetime: # noqa: D102
stat = (self.root / filename).stat()
return datetime.fromtimestamp(stat.st_mtime).astimezone()

def created(self, filename: str) -> datetime: # noqa: D102
stat = (self.root / filename).stat()
try:
return datetime.fromtimestamp(stat.st_birthtime).astimezone() # type: ignore[attr-defined]
except AttributeError:
return datetime.fromtimestamp(stat.st_ctime).astimezone()
Loading

0 comments on commit ce600f6

Please sign in to comment.