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.
- dlt_filesystem-0.18.0/PKG-INFO +114 -0
- dlt_filesystem-0.18.0/README.md +54 -0
- dlt_filesystem-0.18.0/pyproject.toml +113 -0
- dlt_filesystem-0.18.0/setup.cfg +4 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/error.py +14 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/adapter.py +180 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/base.py +89 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/core.py +132 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/error.py +76 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/bson_codec.py +99 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/helpers.py +74 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/iterable_codec.py +505 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/readers.py +1090 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/registry.py +156 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/format/settings.py +1 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/databricks.py +81 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/dropbox.py +50 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/ftp.py +55 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/gdrive.py +56 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/hdfs.py +71 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/http.py +394 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/local.py +139 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/oci.py +61 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/onedrive.py +13 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/oss.py +60 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/r2.py +18 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/sharepoint.py +57 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/smb.py +67 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/webdav.py +79 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/fsspec/webhdfs.py +67 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/impl/remote.py +363 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/impl/util.py +80 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/lister.py +301 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/model.py +324 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/source/router.py +272 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/staging.py +193 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/api.py +13 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/local.py +166 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/model.py +1 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/registry.py +122 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/remote.py +266 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/util.py +88 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/target/writer.py +533 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/testing/stub.py +77 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/testing/writer.py +201 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/util/auth.py +511 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/util/fsspec.py +154 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/util/loader.py +123 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/util/python.py +198 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem/util/web.py +72 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/PKG-INFO +114 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/SOURCES.txt +69 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/dependency_links.txt +1 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/requires.txt +35 -0
- dlt_filesystem-0.18.0/src/dlt_filesystem.egg-info/top_level.txt +1 -0
- dlt_filesystem-0.18.0/tests/test_arrow_readinto.py +246 -0
- dlt_filesystem-0.18.0/tests/test_format_registry.py +236 -0
- dlt_filesystem-0.18.0/tests/test_http_server.py +223 -0
- dlt_filesystem-0.18.0/tests/test_source_discovery.py +114 -0
- dlt_filesystem-0.18.0/tests/test_source_dlt_parity.py +504 -0
- dlt_filesystem-0.18.0/tests/test_source_hints.py +124 -0
- dlt_filesystem-0.18.0/tests/test_source_http.py +958 -0
- dlt_filesystem-0.18.0/tests/test_source_incremental.py +320 -0
- dlt_filesystem-0.18.0/tests/test_source_lister.py +238 -0
- dlt_filesystem-0.18.0/tests/test_source_local.py +224 -0
- dlt_filesystem-0.18.0/tests/test_source_remote.py +786 -0
- dlt_filesystem-0.18.0/tests/test_staging.py +204 -0
- dlt_filesystem-0.18.0/tests/test_target_local.py +185 -0
- dlt_filesystem-0.18.0/tests/test_target_registry.py +207 -0
- dlt_filesystem-0.18.0/tests/test_target_writer.py +498 -0
- 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,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('/')}"
|