floe-python 0.8.1__py3-none-any.whl → 0.8.3__py3-none-any.whl
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.
- floe/_cli.py +2 -2
- floe/config/profile.py +14 -0
- floe/config/validate.py +2 -0
- floe/iceberg/catalog.py +12 -2
- floe/iceberg/sink.py +8 -5
- floe/manifest.py +5 -2
- floe/storage/local.py +1 -0
- floe/write/outputs.py +49 -19
- {floe_python-0.8.1.dist-info → floe_python-0.8.3.dist-info}/METADATA +1 -1
- {floe_python-0.8.1.dist-info → floe_python-0.8.3.dist-info}/RECORD +13 -13
- {floe_python-0.8.1.dist-info → floe_python-0.8.3.dist-info}/WHEEL +0 -0
- {floe_python-0.8.1.dist-info → floe_python-0.8.3.dist-info}/entry_points.txt +0 -0
- {floe_python-0.8.1.dist-info → floe_python-0.8.3.dist-info}/licenses/LICENSE +0 -0
floe/_cli.py
CHANGED
|
@@ -40,7 +40,7 @@ def _realpath(path: str) -> Path:
|
|
|
40
40
|
def _profile(location: str | None) -> ProfileConfig | None:
|
|
41
41
|
if not location:
|
|
42
42
|
return None
|
|
43
|
-
return parse_profile(location if location.startswith("s3://") else _realpath(location)) # fmt: skip
|
|
43
|
+
return parse_profile(location if location.startswith(("s3://", "env:")) else _realpath(location)) # fmt: skip
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def _load(path: Path, profile: ProfileConfig | None) -> RootConfig:
|
|
@@ -83,7 +83,7 @@ def _validate(args: argparse.Namespace) -> int:
|
|
|
83
83
|
print(f" Severity: {e.policy.severity}")
|
|
84
84
|
profile_arg = ""
|
|
85
85
|
if args.profile:
|
|
86
|
-
profile_arg = f" -p {args.profile if args.profile.startswith('s3://') else os.path.realpath(args.profile)}" # fmt: skip
|
|
86
|
+
profile_arg = f" -p {args.profile if args.profile.startswith(('s3://', 'env:')) else os.path.realpath(args.profile)}" # fmt: skip
|
|
87
87
|
print(f"Next: floe run -c {path}{profile_arg}")
|
|
88
88
|
return 0
|
|
89
89
|
|
floe/config/profile.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Environment profiles, ported from `profile/parse.rs` and `profile/validate.rs`."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
@@ -404,7 +405,20 @@ def apply_profile(config: RootConfig, profile: ProfileConfig) -> RootConfig:
|
|
|
404
405
|
return config.model_copy(update={k: v for k, v in blocks.items() if v is not None})
|
|
405
406
|
|
|
406
407
|
|
|
408
|
+
ENV_PREFIX = "env:"
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def read_env_profile(location: str) -> str:
|
|
412
|
+
"""`env:NAME`: the profile YAML held in environment variable `NAME`."""
|
|
413
|
+
name = location.removeprefix(ENV_PREFIX)
|
|
414
|
+
if not (text := os.environ.get(name, "")).strip():
|
|
415
|
+
raise FloeConfigError(f"profile environment variable {name} is unset or empty")
|
|
416
|
+
return text
|
|
417
|
+
|
|
418
|
+
|
|
407
419
|
def _docs(location: str | Path) -> list[object]:
|
|
420
|
+
if isinstance(location, str) and location.startswith(ENV_PREFIX):
|
|
421
|
+
return parse_yaml(read_env_profile(location))
|
|
408
422
|
if isinstance(location, Path) or not location.startswith("s3://"):
|
|
409
423
|
return load_yaml(Path(location))
|
|
410
424
|
try:
|
floe/config/validate.py
CHANGED
|
@@ -330,6 +330,8 @@ def _check_iceberg(
|
|
|
330
330
|
raise _fail(
|
|
331
331
|
entity, f"sink.accepted.iceberg.catalog references unknown catalog {name}"
|
|
332
332
|
)
|
|
333
|
+
if definition.type == "glue" and "{domain}" in (definition.database or "") and not (iceberg.namespace or entity.domain): # fmt: skip
|
|
334
|
+
raise _fail(entity, f"sink.accepted.iceberg.namespace or domain is required for catalog {name} database containing {{domain}}") # fmt: skip
|
|
333
335
|
# Unity is rejected at parse (D1); the registry checked `warehouse_storage`.
|
|
334
336
|
if definition.type == "glue" and entity.sink.write_mode.startswith("merge"):
|
|
335
337
|
# ponytail: merges run through DuckDB, attached to REST catalogs only; attach Glue when a merge needs it
|
floe/iceberg/catalog.py
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import weakref
|
|
5
8
|
from collections.abc import Generator, Sequence
|
|
6
9
|
from contextlib import contextmanager
|
|
7
10
|
from urllib.parse import urlsplit
|
|
@@ -26,6 +29,11 @@ _TRANSFORMS = frozenset({"identity", "year", "month", "day", "hour"})
|
|
|
26
29
|
# first id of a partition field, as in the Iceberg spec
|
|
27
30
|
_PARTITION_FIELD_ID_START = 1000
|
|
28
31
|
DUCKDB_CATALOG = "floe_catalog"
|
|
32
|
+
# the ADR's limits (python-core-for-olf.md, "Supported limits"); DuckDB's own default is 80% of the
|
|
33
|
+
# host's RAM, not the cgroup limit, so a Kubernetes pod is OOM-killed instead of spilling.
|
|
34
|
+
# Size pods at the memory limit + 2 GiB. The spill bound fails a runaway merge before it fills the disk.
|
|
35
|
+
DUCKDB_MEMORY_LIMIT = "4GB"
|
|
36
|
+
DUCKDB_MAX_TEMP_DIRECTORY_SIZE = "50GB"
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
def transform_name(field: PartitionField) -> str:
|
|
@@ -174,8 +182,10 @@ def open_duckdb(
|
|
|
174
182
|
raise FloeConfigError(f"iceberg catalog {definition.name}: merges and compaction require a rest catalog (got {definition.type})") # fmt: skip
|
|
175
183
|
statements = _duckdb_setup(definition, storage)
|
|
176
184
|
with run_error("duckdb catalog attach failed"):
|
|
177
|
-
|
|
178
|
-
con = duckdb.connect()
|
|
185
|
+
spill = tempfile.mkdtemp(prefix="floe-duckdb-")
|
|
186
|
+
con = duckdb.connect(config={"memory_limit": DUCKDB_MEMORY_LIMIT, "temp_directory": spill, "max_temp_directory_size": DUCKDB_MAX_TEMP_DIRECTORY_SIZE}) # fmt: skip
|
|
187
|
+
# the spill directory lives as long as the connection (also on `with` exit and at interpreter exit)
|
|
188
|
+
weakref.finalize(con, shutil.rmtree, spill, True)
|
|
179
189
|
for statement in statements:
|
|
180
190
|
con.execute(statement)
|
|
181
191
|
return con
|
floe/iceberg/sink.py
CHANGED
|
@@ -70,9 +70,12 @@ def resolve_target(storages: StoragesConfig | None, catalogs: CatalogsConfig | N
|
|
|
70
70
|
catalog = next((c for c in (catalogs.definitions if catalogs else []) if c.name == name), None) # fmt: skip
|
|
71
71
|
if catalog is None: # validated; kept for callers that skip validation
|
|
72
72
|
raise FloeConfigError(f"entity.name={entity.name} sink.accepted.iceberg.catalog references unknown catalog {name}") # fmt: skip
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
# what `{domain}` in the database stands for (validated: set when the database has the token)
|
|
74
|
+
scope = sink.iceberg.namespace or entity.domain
|
|
75
|
+
database = catalog.database
|
|
76
|
+
if database is not None:
|
|
77
|
+
database = database.replace("{domain}", catalog_ident(scope or ""))
|
|
78
|
+
namespace = catalog_ident(scope or database or "default")
|
|
76
79
|
table = catalog_ident(sink.iceberg.table or entity.name)
|
|
77
80
|
field = f"entity.name={entity.name} sink.accepted.storage"
|
|
78
81
|
accepted_uri, storage = resolve_path(storages, config_dir, sink.storage, sink.path, field) # fmt: skip
|
|
@@ -85,8 +88,8 @@ def resolve_target(storages: StoragesConfig | None, catalogs: CatalogsConfig | N
|
|
|
85
88
|
else:
|
|
86
89
|
location = accepted_uri
|
|
87
90
|
# Rust: the Glue database holds the table; the namespace only names its location
|
|
88
|
-
if
|
|
89
|
-
namespace =
|
|
91
|
+
if database is not None:
|
|
92
|
+
namespace = database
|
|
90
93
|
return IcebergTarget(catalog, storage, namespace, table, location, accepted_uri) # fmt: skip
|
|
91
94
|
|
|
92
95
|
|
floe/manifest.py
CHANGED
|
@@ -17,7 +17,7 @@ from typing import Any, NamedTuple, cast
|
|
|
17
17
|
from pydantic import BaseModel
|
|
18
18
|
|
|
19
19
|
from floe import __version__
|
|
20
|
-
from floe.config.profile import ProfileConfig
|
|
20
|
+
from floe.config.profile import ENV_PREFIX, ProfileConfig, read_env_profile
|
|
21
21
|
from floe.config.project import ReportConfig, RootConfig
|
|
22
22
|
from floe.config.storage import StoragesConfig
|
|
23
23
|
from floe.config.types import ArchiveTarget, EntityConfig, SinkTarget, SourceConfig
|
|
@@ -464,7 +464,10 @@ def build_manifest(
|
|
|
464
464
|
profile_uri = profile_checksum = None
|
|
465
465
|
if options.profile_arg is not None: # recorded like `config_uri` and its checksum
|
|
466
466
|
# an s3 profile is read from there by the run, so it is recorded as typed
|
|
467
|
-
if options.profile_arg.startswith(
|
|
467
|
+
if options.profile_arg.startswith(ENV_PREFIX): # the Job reads the variable too
|
|
468
|
+
profile_uri = options.profile_arg
|
|
469
|
+
profile_checksum = _sha256(read_env_profile(profile_uri).encode())
|
|
470
|
+
elif options.profile_arg.startswith("s3://"):
|
|
468
471
|
profile_uri = options.profile_arg
|
|
469
472
|
profile_checksum = _sha256(read_s3(profile_uri, "profile"))
|
|
470
473
|
else:
|
floe/storage/local.py
CHANGED
|
@@ -101,6 +101,7 @@ class LocalClient:
|
|
|
101
101
|
try:
|
|
102
102
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
103
103
|
mode = src.stat().st_mode & 0o777
|
|
104
|
+
dst.unlink(missing_ok=True) # not through a symlink or shared inode
|
|
104
105
|
fd = os.open(dst, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
|
105
106
|
os.fchmod(fd, mode) # #779: the open mode applies on create only
|
|
106
107
|
with src.open("rb") as fin, os.fdopen(fd, "wb") as fout:
|
floe/write/outputs.py
CHANGED
|
@@ -4,10 +4,9 @@ import io
|
|
|
4
4
|
import math
|
|
5
5
|
import re
|
|
6
6
|
import uuid
|
|
7
|
-
from collections.abc import
|
|
8
|
-
from contextlib import contextmanager
|
|
7
|
+
from collections.abc import Callable, Generator
|
|
8
|
+
from contextlib import contextmanager, suppress
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
-
from itertools import count
|
|
11
10
|
|
|
12
11
|
import polars as pl
|
|
13
12
|
|
|
@@ -58,15 +57,46 @@ def part_uris(client: StorageClient, base: str, ext: str) -> list[str]:
|
|
|
58
57
|
return sorted(u for u in uris if u.rpartition("/")[0] == base and u.endswith(f".{ext}") and _PART.fullmatch(u.rpartition("/")[2].removesuffix(f".{ext}"))) # fmt: skip
|
|
59
58
|
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
@contextmanager
|
|
61
|
+
def _parts(client: StorageClient, base: str, mode: WriteMode, ext: str) -> Generator[Callable[[bytes], str]]: # fmt: skip
|
|
62
|
+
"""Yield `put(body) -> name`. Non-overwrite modes write `part-<uuid>` directly.
|
|
63
|
+
|
|
64
|
+
Overwrite stages every part as `_staging-<id>-part-NNNNN` (invisible to `part_uris`) and
|
|
65
|
+
only when all were written copies them over `part-NNNNN`, deletes the old parts that no new
|
|
66
|
+
part replaced, then the staged copies (#1054). A failed write leaves the old parts untouched.
|
|
67
|
+
"""
|
|
63
68
|
if mode != "overwrite":
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
|
|
70
|
+
def put(body: bytes) -> str:
|
|
71
|
+
name = f"part-{uuid.uuid4()}.{ext}"
|
|
72
|
+
client.write(f"{base}/{name}", body)
|
|
73
|
+
return name
|
|
74
|
+
|
|
75
|
+
yield put
|
|
76
|
+
return
|
|
77
|
+
stage = f"_staging-{uuid.uuid4()}-"
|
|
78
|
+
names: list[str] = []
|
|
79
|
+
|
|
80
|
+
def put_staged(body: bytes) -> str:
|
|
81
|
+
name = f"part-{len(names):05}.{ext}"
|
|
82
|
+
names.append(name) # before the write: a partial file needs cleanup too
|
|
83
|
+
client.write(f"{base}/{stage}{name}", body)
|
|
84
|
+
return name
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
yield put_staged
|
|
88
|
+
except BaseException:
|
|
89
|
+
for name in names:
|
|
90
|
+
with suppress(FloeError, OSError): # best effort: the original error wins
|
|
91
|
+
client.delete(f"{base}/{stage}{name}")
|
|
92
|
+
raise
|
|
93
|
+
old = part_uris(client, base, ext)
|
|
94
|
+
for name in names:
|
|
95
|
+
client.copy(f"{base}/{stage}{name}", f"{base}/{name}")
|
|
96
|
+
for uri in set(old) - {f"{base}/{n}" for n in names}:
|
|
67
97
|
client.delete(uri)
|
|
68
|
-
for
|
|
69
|
-
|
|
98
|
+
for name in names:
|
|
99
|
+
client.delete(f"{base}/{stage}{name}")
|
|
70
100
|
|
|
71
101
|
|
|
72
102
|
def write_accepted(
|
|
@@ -78,7 +108,6 @@ def write_accepted(
|
|
|
78
108
|
) -> AcceptedWrite:
|
|
79
109
|
"""Write `df` as `part-*.parquet` files under `base`, split so each holds about `max_size_per_file`."""
|
|
80
110
|
base = base.rstrip("/")
|
|
81
|
-
names = _part_names(client, base, mode, "parquet")
|
|
82
111
|
max_size = (options and options.max_size_per_file) or DEFAULT_MAX_SIZE_PER_FILE
|
|
83
112
|
avg_row = max(1, math.ceil(df.estimated_size() / df.height)) if df.height else 1
|
|
84
113
|
rows = max(1, max_size // avg_row)
|
|
@@ -89,14 +118,12 @@ def write_accepted(
|
|
|
89
118
|
kwargs["row_group_size"] = options.row_group_size
|
|
90
119
|
files: list[str] = []
|
|
91
120
|
sizes: list[int] = []
|
|
92
|
-
with _os_errors():
|
|
121
|
+
with _os_errors(), _parts(client, base, mode, "parquet") as put:
|
|
93
122
|
for chunk in [df.slice(i, rows) for i in range(0, df.height, rows)] or [df]:
|
|
94
|
-
name = next(names)
|
|
95
123
|
# ponytail: a part is held in memory (≤ max_size_per_file) before the put; stream multipart uploads if that is too much
|
|
96
124
|
body = io.BytesIO()
|
|
97
125
|
chunk.write_parquet(body, **kwargs) # pyright: ignore[reportArgumentType] # ponytail: kwargs loses the compression Literal; validated by the config
|
|
98
|
-
|
|
99
|
-
files.append(name)
|
|
126
|
+
files.append(put(body.getvalue()))
|
|
100
127
|
sizes.append(body.tell())
|
|
101
128
|
total = sum(sizes)
|
|
102
129
|
return AcceptedWrite(
|
|
@@ -115,7 +142,10 @@ def write_rejected(df: pl.DataFrame, client: StorageClient, base: str, mode: Wri
|
|
|
115
142
|
"""
|
|
116
143
|
with _os_errors():
|
|
117
144
|
base = base.rstrip("/")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
if key and mode != "overwrite":
|
|
146
|
+
name = f"part-{uuid.uuid5(uuid.NAMESPACE_URL, key)}.csv"
|
|
147
|
+
client.write(f"{base}/{name}", df.write_csv().encode())
|
|
148
|
+
else:
|
|
149
|
+
with _parts(client, base, mode, "csv") as put:
|
|
150
|
+
name = put(df.write_csv().encode())
|
|
151
|
+
return f"{base}/{name}"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
floe/__init__.py,sha256=0X9pm8uWiwoDslKZyyjsEAYd6g1mlvIWhkWgE5uhGng,13279
|
|
2
2
|
floe/__main__.py,sha256=Nzz6wrGXRpJiO59qlX7WEJRTlb-XcRFswmP0TA2KjyM,57
|
|
3
|
-
floe/_cli.py,sha256=
|
|
3
|
+
floe/_cli.py,sha256=hwast3uMX0iUtkIpcChSIVHYkoYi4fR4jwCwQT53lXE,14101
|
|
4
4
|
floe/_cli_output.py,sha256=VLxWQr-EPtIwklo9xr2CnjMD17U-7JrMUWdTtAP3jrY,8809
|
|
5
5
|
floe/add_entity.py,sha256=42b7l8o55dzgaR_DDgpW7YWdnU2weYMf4U_rf7qCY6M,21986
|
|
6
6
|
floe/errors.py,sha256=hLSkgPQATBnjY1CAQqX4aB0oI0-WKoHW09bDXAgzC7k,186
|
|
7
7
|
floe/glob.py,sha256=A0HdePLXLGl1T6heJbgdrNHr8pZfdidG7pQ1QsXWOl8,10579
|
|
8
8
|
floe/maintain.py,sha256=CGSoZhO7p0u14o10FCKCO4FprzpVO548A71eq5FowF8,7343
|
|
9
|
-
floe/manifest.py,sha256=
|
|
9
|
+
floe/manifest.py,sha256=sAWjv6yS4GokQDTnZwutyplLz1ICPBYl2qJmm9IORdM,20462
|
|
10
10
|
floe/manifest_run.py,sha256=1XLInkpaCofMezMBs_5uIFoR5W7lbwCBFZeqRcnwUAw,12398
|
|
11
11
|
floe/pii.py,sha256=uJjU-C4c7h45E7rEL9K3Zp1LTTLh-FNKfUV5av2QMO4,4351
|
|
12
12
|
floe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -20,17 +20,17 @@ floe/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
20
20
|
floe/config/entity.py,sha256=CLXLXzkUbQhxenD_LwjBt0eoPreaYaxiBVEsdmnFIco,15883
|
|
21
21
|
floe/config/include.py,sha256=XKYhp2QK_VUmAODAXJ35Wl4FCpQEMQm24m-WKlpI_og,5033
|
|
22
22
|
floe/config/parse.py,sha256=B3gOi80-M0y9Y4OyNjM4MYmM2mFHKGVpqipX1JqaLjM,5290
|
|
23
|
-
floe/config/profile.py,sha256=
|
|
23
|
+
floe/config/profile.py,sha256=jWackztS-0H3dTMXHALTZ7LsIOmwjriwoeY4vFgARr0,16046
|
|
24
24
|
floe/config/project.py,sha256=zgTOhTDlbMr1chViMEZyv0drozZdumTaIoKDL0vv9sw,14141
|
|
25
25
|
floe/config/storage.py,sha256=BK3P65ukFqNgmbjCcuYmXR04OScf1yD8f19vGJq-aDI,6234
|
|
26
26
|
floe/config/types.py,sha256=4eveoOlyGmNxyZpI1PSjli9lnXLPc1-jYj4fL8qHN_8,5038
|
|
27
|
-
floe/config/validate.py,sha256
|
|
27
|
+
floe/config/validate.py,sha256=-cDn9CKfR1BmAATgYy1PWlTWSkpu4mjt4PMKNL7k2FE,35858
|
|
28
28
|
floe/config/vars.py,sha256=7Bq0IU0KrFeRlyA8W9-F0gDSDKfXCfFpBrX08H8Flvw,4577
|
|
29
29
|
floe/config/yaml.py,sha256=HeFGhKFxYkiqQOwljYOgUD5naJyxL_ZIX6hUIS36tzU,10071
|
|
30
|
-
floe/iceberg/catalog.py,sha256=
|
|
30
|
+
floe/iceberg/catalog.py,sha256=QW8L6DKVr4Ug0CbuEP4wgy7Lh5-TsoOXRZWQxfM5zSQ,15115
|
|
31
31
|
floe/iceberg/compact.py,sha256=5O372vAIsvgRc2PGihryAsrQ0otn0F30AQu-cetRCP0,7123
|
|
32
32
|
floe/iceberg/merge.py,sha256=VE1Odf2B34Gxj7ZcuhbrfbgTGM9AdiDz6_2BbcQ-kSQ,12448
|
|
33
|
-
floe/iceberg/sink.py,sha256=
|
|
33
|
+
floe/iceberg/sink.py,sha256=uE0jLhhxRFOeKi7ChAzstKexy1ZrovE4EKATGTYn-Gk,11255
|
|
34
34
|
floe/iceberg/unique.py,sha256=PKUFEcCkU71k66__KfUE7NTos73ZYlZbb8GJPn0yFoo,2958
|
|
35
35
|
floe/iceberg/write.py,sha256=zrFeeJu-YT4Pxo9u-fRujUtFb1Epx1KFoq2qFyP1iGs,11036
|
|
36
36
|
floe/lineage/events.py,sha256=4txd8bGUwKBZd-8Aw_ebTurhlxAYk8SVMgH8vSjC3bs,6682
|
|
@@ -51,16 +51,16 @@ floe/run/events.py,sha256=OE7zYkt0Dz2xrvgNavT1nHfZJ-97IIJ8LpHcameUJZU,2859
|
|
|
51
51
|
floe/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
52
|
floe/storage/archive.py,sha256=1gPImm9PDSJFCpWjg37ax4ubb0wwc6FZm38quQXhORg,1101
|
|
53
53
|
floe/storage/inputs.py,sha256=nmgEo2ub3hPUghLUXHU1wnKzpL4aYZbHb6S0XPYvMew,10410
|
|
54
|
-
floe/storage/local.py,sha256=
|
|
54
|
+
floe/storage/local.py,sha256=UiGFYEY33j45CQspWzRyJBW0HC2Nk9_zG--wL-aUuJI,6388
|
|
55
55
|
floe/storage/partitioning.py,sha256=MG8RJDrwgZhucTswlXC2RwCmzrYIoOE6SH_aBM11KW8,6178
|
|
56
56
|
floe/storage/paths.py,sha256=Z0fry3Gp0Tu2DpUeKqNXdqYP6n_KcKzK2llGx0TGHB8,2343
|
|
57
57
|
floe/storage/resolve.py,sha256=pFAuVytTfnSEcTAVlJjFTj7S_Vipjry1ormWZP3Egmc,2593
|
|
58
58
|
floe/storage/s3.py,sha256=XcG1qJTEY3KLSMLT950mUdrAxuCA8-Gx3fpNSzyV_Xo,7508
|
|
59
59
|
floe/storage/types.py,sha256=FvAEXWMdVdopRK276JyETRYGOva2-uR8pZvsWj6Xvy0,993
|
|
60
60
|
floe/write/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
|
-
floe/write/outputs.py,sha256=
|
|
62
|
-
floe_python-0.8.
|
|
63
|
-
floe_python-0.8.
|
|
64
|
-
floe_python-0.8.
|
|
65
|
-
floe_python-0.8.
|
|
66
|
-
floe_python-0.8.
|
|
61
|
+
floe/write/outputs.py,sha256=mPS3W2bHgeNR_FJ3Ghb13gMgiPpk5-mpSad1bwq97Kg,6034
|
|
62
|
+
floe_python-0.8.3.dist-info/METADATA,sha256=DApXCIMAHZD6G1V_b9v5T2APwgUeKUEfWYmUk10EsK4,442
|
|
63
|
+
floe_python-0.8.3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
64
|
+
floe_python-0.8.3.dist-info/entry_points.txt,sha256=DU75r-E3zcK0quvwg5iT5DeWbUJck_Tz80bEj1IIzHA,40
|
|
65
|
+
floe_python-0.8.3.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
|
|
66
|
+
floe_python-0.8.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|