datakit-data 0.6.0__tar.gz → 0.6.2__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.
- {datakit_data-0.6.0 → datakit_data-0.6.2}/PKG-INFO +1 -1
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/__init__.py +1 -1
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/commands/pull.py +1 -1
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/commands/push.py +1 -1
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/s3.py +19 -62
- {datakit_data-0.6.0 → datakit_data-0.6.2}/pyproject.toml +1 -1
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/commands/__init__.py +0 -0
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/commands/init.py +0 -0
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/commands/status.py +0 -0
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/extra_flags.py +0 -0
- {datakit_data-0.6.0 → datakit_data-0.6.2}/datakit_data/project_mixin.py +0 -0
|
@@ -36,7 +36,7 @@ class Pull(ProjectMixin, CommandHelpers, Command):
|
|
|
36
36
|
if bucket == "":
|
|
37
37
|
self.log.info("No bucket specified in config - no data pulled")
|
|
38
38
|
return
|
|
39
|
-
s3 = S3(user_profile, bucket
|
|
39
|
+
s3 = S3(user_profile, bucket)
|
|
40
40
|
clean_flags = ExtraFlags.convert(parsed_args.args)
|
|
41
41
|
if getattr(parsed_args, 'force', False) is True and '--force' not in clean_flags:
|
|
42
42
|
clean_flags.append('--force')
|
|
@@ -44,7 +44,7 @@ class Push(ProjectMixin, CommandHelpers, Command):
|
|
|
44
44
|
if bucket == "":
|
|
45
45
|
self.log.info("No bucket specified in config - no data pushed")
|
|
46
46
|
return
|
|
47
|
-
s3 = S3(user_profile, bucket
|
|
47
|
+
s3 = S3(user_profile, bucket)
|
|
48
48
|
clean_flags = ExtraFlags.convert(parsed_args.args)
|
|
49
49
|
if getattr(parsed_args, 'force', False) is True and '--force' not in clean_flags:
|
|
50
50
|
clean_flags.append('--force')
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
3
|
from collections import namedtuple
|
|
4
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
5
4
|
from logging import NullHandler
|
|
6
5
|
|
|
7
6
|
import boto3
|
|
8
|
-
from botocore.config import Config
|
|
9
7
|
from botocore.exceptions import BotoCoreError, ClientError
|
|
10
8
|
|
|
11
9
|
|
|
@@ -34,16 +32,9 @@ class S3:
|
|
|
34
32
|
# record is the one S3 itself reports, so it stays comparable to a later head/list probe.
|
|
35
33
|
MULTIPART_THRESHOLD = 8 * 1024 * 1024
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
# across a thread pool. boto3 clients are thread-safe for these calls and release the GIL during
|
|
39
|
-
# network I/O, so threads give near-linear speedup for the many-small-files case without the
|
|
40
|
-
# complexity of asyncio. Callers can override via the max_workers arg (e.g. a config key).
|
|
41
|
-
MAX_WORKERS = 16
|
|
42
|
-
|
|
43
|
-
def __init__(self, aws_user_profile, s3_bucket, max_workers=None):
|
|
35
|
+
def __init__(self, aws_user_profile, s3_bucket):
|
|
44
36
|
self.user_profile = aws_user_profile
|
|
45
37
|
self.bucket = s3_bucket
|
|
46
|
-
self.max_workers = max_workers or self.MAX_WORKERS
|
|
47
38
|
|
|
48
39
|
def push(self, data_dir, s3_path='', extra_flags=None, sync_status_dir=None):
|
|
49
40
|
extra_flags = extra_flags or []
|
|
@@ -57,21 +48,20 @@ class S3:
|
|
|
57
48
|
client = self._client()
|
|
58
49
|
failures = 0
|
|
59
50
|
local_files = {k: v for k, v in self._list_local_files(data_dir).items() if not k.endswith('.synced')}
|
|
60
|
-
# Decide what to transfer (cheap, local-only checks) and log intent up front so the log
|
|
61
|
-
# stays deterministic; the transfers themselves run concurrently below.
|
|
62
|
-
to_upload = []
|
|
63
51
|
for rel_path, local_path in sorted(local_files.items()):
|
|
64
52
|
key = prefix + rel_path
|
|
65
53
|
if not force and self._marker_is_fresh(local_path, rel_path, sync_status_dir):
|
|
66
54
|
logger.info(f"skipped: {local_path}")
|
|
67
55
|
continue
|
|
68
56
|
logger.info(f"upload: {local_path} to s3://{self.bucket}/{key}")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
if not dryrun:
|
|
58
|
+
try:
|
|
59
|
+
etag = self._upload(client, local_path, key, sync_status_dir is not None)
|
|
60
|
+
if sync_status_dir is not None:
|
|
61
|
+
self._create_sync_marker(rel_path, sync_status_dir, etag)
|
|
62
|
+
except (ClientError, BotoCoreError) as e:
|
|
63
|
+
failures += 1
|
|
64
|
+
logger.info(f"\n*** Error ***\n{e}\n")
|
|
75
65
|
if delete:
|
|
76
66
|
remote_keys = self._list_s3_keys(client, prefix)
|
|
77
67
|
remote_rel = {k[len(prefix):] for k in remote_keys}
|
|
@@ -94,9 +84,6 @@ class S3:
|
|
|
94
84
|
client = self._client()
|
|
95
85
|
failures = 0
|
|
96
86
|
remote_objects = self._list_s3_objects(client, prefix)
|
|
97
|
-
# Decide what to transfer (cheap, local-only checks) and log intent up front so the log
|
|
98
|
-
# stays deterministic; the transfers themselves run concurrently below.
|
|
99
|
-
to_download = []
|
|
100
87
|
for rel_path in sorted(remote_objects):
|
|
101
88
|
key = prefix + rel_path
|
|
102
89
|
remote_etag = remote_objects[rel_path].etag
|
|
@@ -107,12 +94,15 @@ class S3:
|
|
|
107
94
|
logger.info(f"skipped: s3://{self.bucket}/{key}")
|
|
108
95
|
continue
|
|
109
96
|
logger.info(f"download: s3://{self.bucket}/{key} to {local_path}")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
97
|
+
if not dryrun:
|
|
98
|
+
os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True)
|
|
99
|
+
try:
|
|
100
|
+
client.download_file(self.bucket, key, local_path)
|
|
101
|
+
if sync_status_dir is not None:
|
|
102
|
+
self._create_sync_marker(rel_path, sync_status_dir, remote_etag)
|
|
103
|
+
except (ClientError, BotoCoreError) as e:
|
|
104
|
+
failures += 1
|
|
105
|
+
logger.info(f"\n*** Error ***\n{e}\n")
|
|
116
106
|
if delete:
|
|
117
107
|
local_files = {k: v for k, v in self._list_local_files(data_dir).items() if not k.endswith('.synced')}
|
|
118
108
|
remote_rel = set(remote_objects)
|
|
@@ -129,10 +119,7 @@ class S3:
|
|
|
129
119
|
|
|
130
120
|
def _client(self):
|
|
131
121
|
session = boto3.Session(profile_name=self.user_profile)
|
|
132
|
-
|
|
133
|
-
# too-small pool (botocore's default is 10) or emit "connection pool is full" warnings.
|
|
134
|
-
config = Config(max_pool_connections=self.max_workers)
|
|
135
|
-
return session.client('s3', config=config)
|
|
122
|
+
return session.client('s3')
|
|
136
123
|
|
|
137
124
|
@staticmethod
|
|
138
125
|
def _normalize_etag(etag):
|
|
@@ -156,36 +143,6 @@ class S3:
|
|
|
156
143
|
client.upload_file(local_path, self.bucket, key)
|
|
157
144
|
return self._object_etag(client, key) if need_etag else None
|
|
158
145
|
|
|
159
|
-
def _run_transfers(self, items, transfer):
|
|
160
|
-
# Run transfer(item) for each item across a thread pool, returning the failure count.
|
|
161
|
-
# Per-item ClientError/BotoCoreError are logged and counted (matching the sequential
|
|
162
|
-
# behavior); all logging happens here on the main thread, so the workers do pure I/O.
|
|
163
|
-
if not items:
|
|
164
|
-
return 0
|
|
165
|
-
failures = 0
|
|
166
|
-
with ThreadPoolExecutor(max_workers=min(self.max_workers, len(items))) as pool:
|
|
167
|
-
futures = [pool.submit(transfer, item) for item in items]
|
|
168
|
-
for future in as_completed(futures):
|
|
169
|
-
try:
|
|
170
|
-
future.result()
|
|
171
|
-
except (ClientError, BotoCoreError) as e:
|
|
172
|
-
failures += 1
|
|
173
|
-
logger.info(f"\n*** Error ***\n{e}\n")
|
|
174
|
-
return failures
|
|
175
|
-
|
|
176
|
-
def _push_one(self, client, item, sync_status_dir):
|
|
177
|
-
rel_path, local_path, key = item
|
|
178
|
-
etag = self._upload(client, local_path, key, sync_status_dir is not None)
|
|
179
|
-
if sync_status_dir is not None:
|
|
180
|
-
self._create_sync_marker(rel_path, sync_status_dir, etag)
|
|
181
|
-
|
|
182
|
-
def _pull_one(self, client, item, sync_status_dir):
|
|
183
|
-
rel_path, key, remote_etag, local_path = item
|
|
184
|
-
os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True)
|
|
185
|
-
client.download_file(self.bucket, key, local_path)
|
|
186
|
-
if sync_status_dir is not None:
|
|
187
|
-
self._create_sync_marker(rel_path, sync_status_dir, remote_etag)
|
|
188
|
-
|
|
189
146
|
def _normalize_prefix(self, s3_path):
|
|
190
147
|
# Returns '' for falsy input so callers can concatenate key segments without a leading slash.
|
|
191
148
|
if not s3_path:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|