dlt-filesystem 0.18.0__tar.gz

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.
Files changed (71) hide show
  1. dlt_filesystem-0.18.0/PKG-INFO +114 -0
  2. dlt_filesystem-0.18.0/README.md +54 -0
  3. dlt_filesystem-0.18.0/pyproject.toml +113 -0
  4. dlt_filesystem-0.18.0/setup.cfg +4 -0
  5. dlt_filesystem-0.18.0/src/dlt_filesystem/error.py +14 -0
  6. dlt_filesystem-0.18.0/src/dlt_filesystem/source/adapter.py +180 -0
  7. dlt_filesystem-0.18.0/src/dlt_filesystem/source/base.py +89 -0
  8. dlt_filesystem-0.18.0/src/dlt_filesystem/source/core.py +132 -0
  9. dlt_filesystem-0.18.0/src/dlt_filesystem/source/error.py +76 -0
  10. dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/bson_codec.py +99 -0
  11. dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/helpers.py +74 -0
  12. dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/iterable_codec.py +505 -0
  13. dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/readers.py +1090 -0
  14. dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/registry.py +156 -0
  15. dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/settings.py +1 -0
  16. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/databricks.py +81 -0
  17. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/dropbox.py +50 -0
  18. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/ftp.py +55 -0
  19. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/gdrive.py +56 -0
  20. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/hdfs.py +71 -0
  21. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/http.py +394 -0
  22. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/local.py +139 -0
  23. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/oci.py +61 -0
  24. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/onedrive.py +13 -0
  25. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/oss.py +60 -0
  26. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/r2.py +18 -0
  27. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/sharepoint.py +57 -0
  28. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/smb.py +67 -0
  29. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/webdav.py +79 -0
  30. dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/webhdfs.py +67 -0
  31. dlt_filesystem-0.18.0/src/dlt_filesystem/source/impl/remote.py +363 -0
  32. dlt_filesystem-0.18.0/src/dlt_filesystem/source/impl/util.py +80 -0
  33. dlt_filesystem-0.18.0/src/dlt_filesystem/source/lister.py +301 -0
  34. dlt_filesystem-0.18.0/src/dlt_filesystem/source/model.py +324 -0
  35. dlt_filesystem-0.18.0/src/dlt_filesystem/source/router.py +272 -0
  36. dlt_filesystem-0.18.0/src/dlt_filesystem/staging.py +193 -0
  37. dlt_filesystem-0.18.0/src/dlt_filesystem/target/api.py +13 -0
  38. dlt_filesystem-0.18.0/src/dlt_filesystem/target/local.py +166 -0
  39. dlt_filesystem-0.18.0/src/dlt_filesystem/target/model.py +1 -0
  40. dlt_filesystem-0.18.0/src/dlt_filesystem/target/registry.py +122 -0
  41. dlt_filesystem-0.18.0/src/dlt_filesystem/target/remote.py +266 -0
  42. dlt_filesystem-0.18.0/src/dlt_filesystem/target/util.py +88 -0
  43. dlt_filesystem-0.18.0/src/dlt_filesystem/target/writer.py +533 -0
  44. dlt_filesystem-0.18.0/src/dlt_filesystem/testing/stub.py +77 -0
  45. dlt_filesystem-0.18.0/src/dlt_filesystem/testing/writer.py +201 -0
  46. dlt_filesystem-0.18.0/src/dlt_filesystem/util/auth.py +511 -0
  47. dlt_filesystem-0.18.0/src/dlt_filesystem/util/fsspec.py +154 -0
  48. dlt_filesystem-0.18.0/src/dlt_filesystem/util/loader.py +123 -0
  49. dlt_filesystem-0.18.0/src/dlt_filesystem/util/python.py +198 -0
  50. dlt_filesystem-0.18.0/src/dlt_filesystem/util/web.py +72 -0
  51. dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/PKG-INFO +114 -0
  52. dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/SOURCES.txt +69 -0
  53. dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/dependency_links.txt +1 -0
  54. dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/requires.txt +35 -0
  55. dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/top_level.txt +1 -0
  56. dlt_filesystem-0.18.0/tests/test_arrow_readinto.py +246 -0
  57. dlt_filesystem-0.18.0/tests/test_format_registry.py +236 -0
  58. dlt_filesystem-0.18.0/tests/test_http_server.py +223 -0
  59. dlt_filesystem-0.18.0/tests/test_source_discovery.py +114 -0
  60. dlt_filesystem-0.18.0/tests/test_source_dlt_parity.py +504 -0
  61. dlt_filesystem-0.18.0/tests/test_source_hints.py +124 -0
  62. dlt_filesystem-0.18.0/tests/test_source_http.py +958 -0
  63. dlt_filesystem-0.18.0/tests/test_source_incremental.py +320 -0
  64. dlt_filesystem-0.18.0/tests/test_source_lister.py +238 -0
  65. dlt_filesystem-0.18.0/tests/test_source_local.py +224 -0
  66. dlt_filesystem-0.18.0/tests/test_source_remote.py +786 -0
  67. dlt_filesystem-0.18.0/tests/test_staging.py +204 -0
  68. dlt_filesystem-0.18.0/tests/test_target_local.py +185 -0
  69. dlt_filesystem-0.18.0/tests/test_target_registry.py +207 -0
  70. dlt_filesystem-0.18.0/tests/test_target_writer.py +498 -0
  71. dlt_filesystem-0.18.0/tests/test_util.py +150 -0
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: dlt-filesystem
3
+ Version: 0.18.0
4
+ Summary: Filesystem and blob storage sources and destinations for dlt, across 13 file formats.
5
+ Maintainer-email: Andreas Motl <andreas.motl@panodata.org>
6
+ License-Expression: MIT
7
+ Project-URL: Issues, https://github.com/panodata/omniload/issues
8
+ Project-URL: Repository, https://github.com/panodata/omniload
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Operating System :: Unix
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Database
27
+ Classifier: Topic :: Internet
28
+ Classifier: Topic :: Software Development :: Libraries
29
+ Classifier: Topic :: System :: Archiving
30
+ Classifier: Topic :: Utilities
31
+ Requires-Python: >=3.10
32
+ Description-Content-Type: text/markdown
33
+ Requires-Dist: adlfs
34
+ Requires-Dist: azure-storage-blob
35
+ Requires-Dist: dlt<1.31,>=1.22
36
+ Requires-Dist: dropboxdrivefs
37
+ Requires-Dist: fsspec<2026.10,>=2024.6
38
+ Requires-Dist: fsspec-databricks>=0.1.10
39
+ Requires-Dist: gcsfs
40
+ Requires-Dist: gdrive-fsspec
41
+ Requires-Dist: lakefs-spec<0.16
42
+ Requires-Dist: msgraphfs
43
+ Requires-Dist: ocifs
44
+ Requires-Dist: ossfs
45
+ Requires-Dist: paramiko
46
+ Requires-Dist: pyarrow<26,>=18
47
+ Requires-Dist: s3fs>=2024.6
48
+ Requires-Dist: smbprotocol; platform_system == "Linux"
49
+ Requires-Dist: smbprotocol[kerberos]; platform_system != "Linux"
50
+ Requires-Dist: verlib2
51
+ Requires-Dist: webdav4[fsspec]
52
+ Provides-Extra: iterable
53
+ Requires-Dist: cbor2<7; extra == "iterable"
54
+ Requires-Dist: iterabledata<2,>=1.0.15; extra == "iterable"
55
+ Requires-Dist: lxml<7; extra == "iterable"
56
+ Requires-Dist: msgpack<2; extra == "iterable"
57
+ Requires-Dist: pyyaml<7; extra == "iterable"
58
+ Provides-Extra: vortex
59
+ Requires-Dist: vortex-data<1,>=0.86; python_version >= "3.11" and extra == "vortex"
60
+
61
+ # dlt-filesystem
62
+
63
+ Filesystem and blob storage sources and destinations for
64
+ [dlt](https://github.com/dlt-hub/dlt).
65
+
66
+ `dlt` ships its own filesystem source, and this package is the delta over it:
67
+
68
+ - **16 readers behind 19 routed format keys**, against dlt's four. CSV (pyarrow,
69
+ DuckDB and a headerless variant), JSON and JSONL, Parquet, ORC, Avro, Feather,
70
+ BSON, spreadsheets (`xlsx` and `ods`), XML, YAML, MessagePack and CBOR.
71
+ - **A `filesystem` resource that refuses to load nothing.** A concrete selection
72
+ matching no file raises rather than returning an empty table; globs stay
73
+ empty-safe.
74
+ - **A modification-date resolver that covers the schemes dlt's table does not**,
75
+ including `r2`, `oss`, `hdfs`, `smb`, `ftp` and `webdav`, and a pyarrow-backed
76
+ client addressed as `s3://`.
77
+ - **An Arrow `readinto` shim**, without which reading a `.gz` fails wherever
78
+ `isal` is importable.
79
+
80
+ The entry points are a superset of dlt's own, so a pipeline already on
81
+ `dlt.sources.filesystem` can move across without changing its call.
82
+
83
+ ## Install
84
+
85
+ ```shell
86
+ pip install dlt-filesystem
87
+ ```
88
+
89
+ The long-tail formats (XML, YAML, MessagePack, CBOR) carry their decoders in an
90
+ extra:
91
+
92
+ ```shell
93
+ pip install 'dlt-filesystem[iterable]'
94
+ ```
95
+
96
+ ## Usage
97
+
98
+ ```python
99
+ import dlt
100
+ from dlt_filesystem.source.adapter import readers
101
+
102
+ pipeline = dlt.pipeline(destination="duckdb", dataset_name="inbox")
103
+ pipeline.run(
104
+ readers(bucket_url="s3://bucket/prefix", file_glob="*.parquet").read_parquet()
105
+ )
106
+ ```
107
+
108
+ ## Documentation
109
+
110
+ <https://omniload.readthedocs.io/supported-sources/filesystem.html>
111
+
112
+ ## License
113
+
114
+ MIT. See [LICENSE](https://github.com/panodata/omniload/blob/main/LICENSE).
@@ -0,0 +1,54 @@
1
+ # dlt-filesystem
2
+
3
+ Filesystem and blob storage sources and destinations for
4
+ [dlt](https://github.com/dlt-hub/dlt).
5
+
6
+ `dlt` ships its own filesystem source, and this package is the delta over it:
7
+
8
+ - **16 readers behind 19 routed format keys**, against dlt's four. CSV (pyarrow,
9
+ DuckDB and a headerless variant), JSON and JSONL, Parquet, ORC, Avro, Feather,
10
+ BSON, spreadsheets (`xlsx` and `ods`), XML, YAML, MessagePack and CBOR.
11
+ - **A `filesystem` resource that refuses to load nothing.** A concrete selection
12
+ matching no file raises rather than returning an empty table; globs stay
13
+ empty-safe.
14
+ - **A modification-date resolver that covers the schemes dlt's table does not**,
15
+ including `r2`, `oss`, `hdfs`, `smb`, `ftp` and `webdav`, and a pyarrow-backed
16
+ client addressed as `s3://`.
17
+ - **An Arrow `readinto` shim**, without which reading a `.gz` fails wherever
18
+ `isal` is importable.
19
+
20
+ The entry points are a superset of dlt's own, so a pipeline already on
21
+ `dlt.sources.filesystem` can move across without changing its call.
22
+
23
+ ## Install
24
+
25
+ ```shell
26
+ pip install dlt-filesystem
27
+ ```
28
+
29
+ The long-tail formats (XML, YAML, MessagePack, CBOR) carry their decoders in an
30
+ extra:
31
+
32
+ ```shell
33
+ pip install 'dlt-filesystem[iterable]'
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ import dlt
40
+ from dlt_filesystem.source.adapter import readers
41
+
42
+ pipeline = dlt.pipeline(destination="duckdb", dataset_name="inbox")
43
+ pipeline.run(
44
+ readers(bucket_url="s3://bucket/prefix", file_glob="*.parquet").read_parquet()
45
+ )
46
+ ```
47
+
48
+ ## Documentation
49
+
50
+ <https://omniload.readthedocs.io/supported-sources/filesystem.html>
51
+
52
+ ## License
53
+
54
+ MIT. See [LICENSE](https://github.com/panodata/omniload/blob/main/LICENSE).
@@ -0,0 +1,113 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [
4
+ "setuptools>=77",
5
+ "versioningit",
6
+ ]
7
+
8
+ [project]
9
+ name = "dlt-filesystem"
10
+ description = "Filesystem and blob storage sources and destinations for dlt, across 13 file formats."
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ maintainers = [
14
+ { name = "Andreas Motl", email = "andreas.motl@panodata.org" },
15
+ ]
16
+ requires-python = ">=3.10"
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Information Technology",
21
+ "Intended Audience :: Science/Research",
22
+ "Intended Audience :: System Administrators",
23
+ "Operating System :: MacOS :: MacOS X",
24
+ "Operating System :: Microsoft :: Windows",
25
+ "Operating System :: OS Independent",
26
+ "Operating System :: POSIX :: Linux",
27
+ "Operating System :: Unix",
28
+ "Programming Language :: Python",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
+ "Topic :: Database",
36
+ "Topic :: Internet",
37
+ "Topic :: Software Development :: Libraries",
38
+ "Topic :: System :: Archiving",
39
+ "Topic :: Utilities",
40
+ ]
41
+ dynamic = [ "version" ]
42
+ # `dlt` plus what reading and writing files over fsspec costs. Everything a single
43
+ # format or transport needs beyond this is imported inside the function that needs
44
+ # it, so an install carries the transports and pays for a format only on use.
45
+ dependencies = [
46
+ "adlfs",
47
+ "azure-storage-blob",
48
+ "dlt>=1.22,<1.31",
49
+ "dropboxdrivefs",
50
+ "fsspec>=2024.6,<2026.10",
51
+ "fsspec-databricks>=0.1.10",
52
+ "gcsfs",
53
+ "gdrive-fsspec",
54
+ "lakefs-spec<0.16",
55
+ "msgraphfs",
56
+ "ocifs",
57
+ "ossfs",
58
+ "paramiko",
59
+ "pyarrow>=18,<26",
60
+ "s3fs>=2024.6",
61
+ "smbprotocol; platform_system=='Linux'",
62
+ "smbprotocol[kerberos]; platform_system!='Linux'",
63
+ "verlib2",
64
+ "webdav4[fsspec]",
65
+ ]
66
+ # Long-tail file formats (MessagePack via iterabledata; CBOR via cbor2, XML via lxml, YAML via
67
+ # PyYAML directly). iterabledata core is light (chardet, tqdm); each format's decoder is pulled
68
+ # explicitly here (msgpack, cbor2, lxml, pyyaml). lxml and pyyaml are commonly present
69
+ # transitively but are declared so the extra is self-contained. Note: iterabledata's import
70
+ # package name is the generic `iterable`, which can shadow a same-named local package.
71
+ # See dlt_filesystem/source/format/iterable_codec.py.
72
+ optional-dependencies.iterable = [
73
+ "cbor2<7",
74
+ "iterabledata>=1.0.15,<2",
75
+ "lxml<7",
76
+ "msgpack<2",
77
+ "pyyaml<7",
78
+ ]
79
+ # Vortex columnar files. vortex-data is a ~119 MB install that publishes wheels for Python
80
+ # 3.11+ only, so it is opt-in rather than carried by every install of this package.
81
+ optional-dependencies.vortex = [
82
+ "vortex-data>=0.86,<1; python_version>='3.11'",
83
+ ]
84
+ urls.Issues = "https://github.com/panodata/omniload/issues"
85
+ urls.Repository = "https://github.com/panodata/omniload"
86
+
87
+ # The package carries no `__init__.py` at any level, so discovery has to be told that
88
+ # a directory without one is still a package. Without this, setuptools finds nothing
89
+ # and builds an empty wheel that `twine check` reports as PASSED.
90
+ [tool.setuptools]
91
+ packages.find.where = [ "src" ]
92
+ packages.find.namespaces = true
93
+ package-dir = { "" = "src" }
94
+
95
+ [tool.pytest]
96
+ ini_options.minversion = "2.0"
97
+ ini_options.testpaths = [
98
+ "tests",
99
+ ]
100
+ ini_options.addopts = """
101
+ -rfEXsS --strict-markers --verbosity=3
102
+ """
103
+ # Declared here as well as in the consumer's configuration, because a run rooted at
104
+ # this project reads this file alone and `--strict-markers` fails on an unknown one.
105
+ ini_options.markers = [
106
+ "integration: requires Docker/testcontainers or external credentials",
107
+ ]
108
+ ini_options.xfail_strict = true
109
+
110
+ [tool.versioningit]
111
+ vcs.method = "git-archive"
112
+ vcs.default-tag = "v0.0.0"
113
+ vcs.describe-subst = "$Format:%(describe:tags,match=v*)$"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class MissingConnectorOption(Exception):
5
+ def __init__(self, option, connector):
6
+ super().__init__(f"{option} is required to connect to {connector}")
7
+
8
+
9
+ class InvalidBlobTableError(Exception):
10
+ def __init__(self, source):
11
+ super().__init__(
12
+ f"Invalid source table for: {source}. "
13
+ "Ensure that the table is in the format {bucket-name}/{file glob}"
14
+ )
@@ -0,0 +1,180 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Reads files in s3, gs or azure buckets using fsspec and provides convenience resources for chunked reading of various file formats"""
16
+
17
+ from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
18
+
19
+ import dlt
20
+ from dlt.sources import DltResource
21
+ from dlt.sources.credentials import FileSystemCredentials
22
+ from dlt.sources.filesystem import FileItem, FileItemDict, fsspec_filesystem
23
+ from fsspec import AbstractFileSystem
24
+
25
+ from dlt_filesystem.source.error import NoFilesFoundError
26
+ from dlt_filesystem.source.format import readers as reader_functions
27
+ from dlt_filesystem.source.format.readers import ReadersSource
28
+ from dlt_filesystem.source.format.registry import (
29
+ READER_REGISTRATIONS,
30
+ ReaderRegistration,
31
+ )
32
+ from dlt_filesystem.source.lister import glob_files
33
+
34
+ from .model import FilesystemConfigurationResource
35
+
36
+
37
+ def _resolve_reader(registration: ReaderRegistration):
38
+ """Resolve a registered reader name without making the registry import reader code."""
39
+ reader = getattr(reader_functions, registration.reader_name, None)
40
+ if not callable(reader):
41
+ raise ValueError(
42
+ f"Reader function {registration.reader_name!r} is not defined in "
43
+ "dlt_filesystem.source.format.readers"
44
+ )
45
+ return reader
46
+
47
+
48
+ @dlt.source(_impl_cls=ReadersSource, spec=FilesystemConfigurationResource)
49
+ def readers(
50
+ bucket_url: str = dlt.secrets.value,
51
+ credentials: Union[FileSystemCredentials, AbstractFileSystem] = dlt.secrets.value,
52
+ file_glob: Optional[str] = "*",
53
+ *,
54
+ kwargs: Optional[Dict[str, Any]] = None,
55
+ client_kwargs: Optional[Dict[str, Any]] = None,
56
+ incremental: Optional[dlt.sources.incremental[Any]] = None,
57
+ ) -> Tuple[DltResource, ...]:
58
+ """This source provides a few resources that are chunked file readers. Readers can be further parametrized before use
59
+ read_csv(chunksize, **pandas_kwargs)
60
+ read_json(chunksize)
61
+ read_jsonl(chunksize)
62
+ read_parquet(chunksize)
63
+
64
+ Args:
65
+ bucket_url (str): The url to the bucket.
66
+ credentials (FileSystemCredentials | AbstractFilesystem): The credentials to the filesystem of fsspec `AbstractFilesystem` instance.
67
+ file_glob (str, optional): The filter to apply to the files in glob format. by default lists all files in bucket_url non-recursively
68
+ kwargs (Optional[Dict[str, Any]]): Additional arguments passed to the fsspec constructor, ie. dict(use_ssl=True) for s3fs
69
+ client_kwargs (Optional[Dict[str, Any]]): Additional arguments passed to the underlying fsspec native client, ie. dict(verify="public.crt") for botocore
70
+ incremental (Optional[dlt.sources.incremental[Any]]): Defines an incremental cursor on the listed files, with `modification_date`
71
+ being the most common choice, which returns only files created since the previous run.
72
+ """
73
+ filesystem_resource = filesystem(
74
+ bucket_url,
75
+ credentials,
76
+ file_glob=file_glob,
77
+ kwargs=kwargs,
78
+ client_kwargs=client_kwargs,
79
+ incremental=incremental,
80
+ )
81
+
82
+ return tuple(
83
+ filesystem_resource
84
+ | dlt.transformer(
85
+ name=registration.reader_name,
86
+ max_table_nesting=registration.max_table_nesting,
87
+ )(_resolve_reader(registration))
88
+ for registration in READER_REGISTRATIONS
89
+ )
90
+
91
+
92
+ @dlt.resource(
93
+ primary_key="file_url", spec=FilesystemConfigurationResource, standalone=True
94
+ )
95
+ def filesystem(
96
+ bucket_url: str = dlt.secrets.value,
97
+ credentials: Union[FileSystemCredentials, AbstractFileSystem] = dlt.secrets.value,
98
+ file_glob: Optional[str] = "*",
99
+ files_per_page: int = 100,
100
+ extract_content: bool = False,
101
+ require_file_match: bool = False,
102
+ filesystem_incremental: bool = False,
103
+ *,
104
+ kwargs: Optional[Dict[str, Any]] = None,
105
+ client_kwargs: Optional[Dict[str, Any]] = None,
106
+ incremental: Optional[dlt.sources.incremental[Any]] = None,
107
+ ) -> Iterator[List[FileItem]]:
108
+ """This resource lists files in `bucket_url` using `file_glob` pattern. The files are yielded as FileItem which also
109
+ provide methods to open and read file data. It should be combined with transformers that further process (ie. load files)
110
+
111
+ Args:
112
+ bucket_url (str): The url to the bucket.
113
+ credentials (FileSystemCredentials | AbstractFilesystem): The credentials to the filesystem of fsspec `AbstractFilesystem` instance.
114
+ file_glob (str, optional): The filter to apply to the files in glob format. by default lists all files in bucket_url non-recursively
115
+ files_per_page (int, optional): The number of files to process at once, defaults to 100.
116
+ extract_content (bool, optional): If true, the content of the file will be extracted if
117
+ false it will return a fsspec file, defaults to False.
118
+ require_file_match (bool, optional): Raise when the concrete source selection
119
+ matches no file. Defaults to False for direct uses of this resource.
120
+ filesystem_incremental (bool, optional): Resolve trustworthy modification
121
+ times when the listing itself does not carry one. Defaults to False.
122
+ kwargs (Optional[Dict[str, Any]]): Additional arguments passed to the fsspec constructor, ie. dict(use_ssl=True) for s3fs
123
+ client_kwargs (Optional[Dict[str, Any]]): Additional arguments passed to the underlying fsspec native client, ie. dict(verify="public.crt") for botocore
124
+ incremental (Optional[dlt.sources.incremental[Any]]): Defines an incremental cursor on the listed files, with `modification_date`
125
+ being the most common choice, which returns only files created since the previous run.
126
+ A cursor carrying `row_order` also orders the listing by its cursor field.
127
+
128
+ Returns:
129
+ Iterator[List[FileItem]]: The list of files.
130
+ """
131
+
132
+ fs_client: AbstractFileSystem
133
+ if isinstance(credentials, AbstractFileSystem):
134
+ # A caller who hands over a constructed filesystem has already spent
135
+ # `kwargs` and `client_kwargs` on building it, so both are ignored here,
136
+ # exactly as they are in dlt's own resource.
137
+ fs_client = credentials
138
+ else:
139
+ fs_client = fsspec_filesystem(
140
+ bucket_url, credentials, kwargs=kwargs, client_kwargs=client_kwargs
141
+ )[0]
142
+
143
+ file_models: Iterable[FileItem] = glob_files(
144
+ fs_client,
145
+ bucket_url,
146
+ file_glob or "**",
147
+ filesystem_incremental=filesystem_incremental,
148
+ )
149
+ if incremental and incremental.row_order:
150
+ # `row_order` is ascending or descending *in the direction `last_value_func`
151
+ # advances*, so it maps onto a raw sort only through that function: `max`
152
+ # advances upwards and `min` advances downwards, which inverts the
153
+ # comparison for `min`. Mirrors dlt's own expression.
154
+ reverse = (
155
+ incremental.row_order == "asc" and incremental.last_value_func is min
156
+ ) or (incremental.row_order == "desc" and incremental.last_value_func is max)
157
+ # The listing has to be materialised to be ordered. Only this branch pays
158
+ # for it; the default stays lazy.
159
+ file_models = sorted(
160
+ file_models,
161
+ key=lambda listed: listed[incremental.cursor_path], # ty: ignore[invalid-key]
162
+ reverse=reverse,
163
+ )
164
+
165
+ matched_files = 0
166
+ files_chunk: List[FileItem] = []
167
+ for file_model in file_models:
168
+ matched_files += 1
169
+ file_dict = FileItemDict(file_model, fs_client)
170
+ if extract_content:
171
+ file_dict["file_content"] = file_dict.read_bytes()
172
+ files_chunk.append(file_dict) # ty: ignore[invalid-argument-type]
173
+ # wait for the chunk to be full
174
+ if len(files_chunk) >= files_per_page:
175
+ yield files_chunk
176
+ files_chunk = []
177
+ if require_file_match and matched_files == 0:
178
+ raise NoFilesFoundError(bucket_url, file_glob or "**")
179
+ if files_chunk:
180
+ yield files_chunk
@@ -0,0 +1,89 @@
1
+ from typing import Union
2
+ from urllib.parse import urlparse
3
+
4
+
5
+ class FilesystemSource:
6
+ """Shared capabilities for the filesystem-family sources.
7
+
8
+ Covers the local ``file://`` source and every remote transport
9
+ (``s3://``, ``gs://``, ``az://`` / ``adls://`` / ``abfss://``, ``sftp://``),
10
+ which all converge on the same reader after URI parsing.
11
+
12
+ Filesystem sources manage their own incremental behaviour
13
+ (``handles_incrementality`` is ``True``) and support opt-in file selection by
14
+ modification time (``supports_filesystem_incremental`` is ``True``). They
15
+ carry no resource-level write disposition, so a run-level disposition is safe
16
+ to apply: ``run_ingest`` honours an explicit ``--incremental-strategy append``
17
+ / ``replace`` for them (``honours_run_disposition`` is ``True``). Sources that
18
+ set their own resource-level disposition leave this ``False`` (the default)
19
+ so the run-level value never overrides theirs.
20
+ """
21
+
22
+ def handles_incrementality(self) -> bool:
23
+ return True
24
+
25
+ def honours_run_disposition(self) -> bool:
26
+ return True
27
+
28
+ def consumed_run_options(self) -> frozenset:
29
+ """Return the run options this source's ``dlt_source`` accepts by name.
30
+
31
+ Every other name in omniload's run vocabulary is filtered out before the
32
+ call, so it never reaches an fsspec or Arrow constructor as a stray
33
+ keyword. The two named here are resource options, not connector ones:
34
+ they configure how the reader resource is built (``FilesystemReference``)
35
+ and every ``dlt_source`` implementation in this family declares them
36
+ explicitly rather than reading them out of ``**kwargs``.
37
+ """
38
+ return frozenset({"filesystem_incremental", "column_types"})
39
+
40
+ def supports_filesystem_incremental(self) -> bool:
41
+ """Return whether the source supports file-level mtime selection."""
42
+ return True
43
+
44
+ def produces_multiple_tables(self, uri: str, table: str) -> bool:
45
+ """Return whether a workbook selection dispatches worksheet tables."""
46
+ from dlt_filesystem.source.error import UnsupportedEndpointError
47
+ from dlt_filesystem.source.format.readers import (
48
+ spreadsheet_selection_is_plural,
49
+ )
50
+ from dlt_filesystem.source.router import (
51
+ blob_hints,
52
+ determine_endpoint,
53
+ parse_uri,
54
+ )
55
+
56
+ parsed_uri = urlparse(uri)
57
+ _, path = parse_uri(parsed_uri, table)
58
+ try:
59
+ endpoint = determine_endpoint(table, path)
60
+ except (UnsupportedEndpointError, ValueError):
61
+ return False
62
+ return endpoint in {
63
+ "read_excel",
64
+ "read_ods",
65
+ } and spreadsheet_selection_is_plural(blob_hints(parsed_uri, table))
66
+
67
+ @staticmethod
68
+ def endpoint_namespace(endpoint: Union[str, None], default: str) -> str:
69
+ """
70
+ Return a normalized endpoint identity without credentials or query values.
71
+ It is used for incremental loading based on file modification times.
72
+
73
+ # TODO: Remove `default` argument again?
74
+ """
75
+ if not endpoint:
76
+ return default
77
+
78
+ parsed = urlparse(endpoint if "://" in endpoint else f"//{endpoint}")
79
+ host = parsed.hostname
80
+ if not host:
81
+ return default
82
+
83
+ host = host.lower()
84
+ if ":" in host:
85
+ host = f"[{host}]"
86
+ if parsed.port is not None:
87
+ host = f"{host}:{parsed.port}"
88
+
89
+ return f"{host}{parsed.path.rstrip('/')}"