datakit-data 0.6.1__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.1 → datakit_data-0.6.2}/PKG-INFO +1 -1
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/__init__.py +1 -1
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/commands/pull.py +1 -1
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/commands/push.py +1 -1
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/s3.py +20 -74
- {datakit_data-0.6.1 → datakit_data-0.6.2}/pyproject.toml +1 -1
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/commands/__init__.py +0 -0
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/commands/init.py +0 -0
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/commands/status.py +0 -0
- {datakit_data-0.6.1 → datakit_data-0.6.2}/datakit_data/extra_flags.py +0 -0
- {datakit_data-0.6.1 → 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,12 +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 boto3.s3.transfer import TransferConfig
|
|
9
|
-
from botocore.config import Config
|
|
10
7
|
from botocore.exceptions import BotoCoreError, ClientError
|
|
11
8
|
|
|
12
9
|
|
|
@@ -35,26 +32,9 @@ class S3:
|
|
|
35
32
|
# record is the one S3 itself reports, so it stays comparable to a later head/list probe.
|
|
36
33
|
MULTIPART_THRESHOLD = 8 * 1024 * 1024
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
# across a thread pool. boto3 clients are thread-safe for these calls and release the GIL during
|
|
40
|
-
# network I/O, so threads give near-linear speedup for the many-small-files case without the
|
|
41
|
-
# complexity of asyncio. Callers can override via the max_workers arg (e.g. a config key).
|
|
42
|
-
MAX_WORKERS = 16
|
|
43
|
-
|
|
44
|
-
# boto3's managed transfers (upload_file/download_file) each spin up their own internal thread
|
|
45
|
-
# pool (TransferConfig.max_concurrency, default 10), every thread of which borrows a connection
|
|
46
|
-
# from the shared pool. Because we already parallelize across files in our own thread pool, that
|
|
47
|
-
# inner concurrency is redundant and multiplies connection demand to max_workers * 10 — far past
|
|
48
|
-
# the max_pool_connections=max_workers pool, triggering urllib3 "connection pool is full"
|
|
49
|
-
# warnings and pathological TLS-handshake churn. Disabling inner threads makes each managed
|
|
50
|
-
# transfer use a single connection, so total demand equals the worker count and the pool matches.
|
|
51
|
-
# Large files still upload/download via multipart (parts just go sequentially) for resilience.
|
|
52
|
-
_TRANSFER_CONFIG = TransferConfig(use_threads=False)
|
|
53
|
-
|
|
54
|
-
def __init__(self, aws_user_profile, s3_bucket, max_workers=None):
|
|
35
|
+
def __init__(self, aws_user_profile, s3_bucket):
|
|
55
36
|
self.user_profile = aws_user_profile
|
|
56
37
|
self.bucket = s3_bucket
|
|
57
|
-
self.max_workers = max_workers or self.MAX_WORKERS
|
|
58
38
|
|
|
59
39
|
def push(self, data_dir, s3_path='', extra_flags=None, sync_status_dir=None):
|
|
60
40
|
extra_flags = extra_flags or []
|
|
@@ -68,21 +48,20 @@ class S3:
|
|
|
68
48
|
client = self._client()
|
|
69
49
|
failures = 0
|
|
70
50
|
local_files = {k: v for k, v in self._list_local_files(data_dir).items() if not k.endswith('.synced')}
|
|
71
|
-
# Decide what to transfer (cheap, local-only checks) and log intent up front so the log
|
|
72
|
-
# stays deterministic; the transfers themselves run concurrently below.
|
|
73
|
-
to_upload = []
|
|
74
51
|
for rel_path, local_path in sorted(local_files.items()):
|
|
75
52
|
key = prefix + rel_path
|
|
76
53
|
if not force and self._marker_is_fresh(local_path, rel_path, sync_status_dir):
|
|
77
54
|
logger.info(f"skipped: {local_path}")
|
|
78
55
|
continue
|
|
79
56
|
logger.info(f"upload: {local_path} to s3://{self.bucket}/{key}")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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")
|
|
86
65
|
if delete:
|
|
87
66
|
remote_keys = self._list_s3_keys(client, prefix)
|
|
88
67
|
remote_rel = {k[len(prefix):] for k in remote_keys}
|
|
@@ -105,9 +84,6 @@ class S3:
|
|
|
105
84
|
client = self._client()
|
|
106
85
|
failures = 0
|
|
107
86
|
remote_objects = self._list_s3_objects(client, prefix)
|
|
108
|
-
# Decide what to transfer (cheap, local-only checks) and log intent up front so the log
|
|
109
|
-
# stays deterministic; the transfers themselves run concurrently below.
|
|
110
|
-
to_download = []
|
|
111
87
|
for rel_path in sorted(remote_objects):
|
|
112
88
|
key = prefix + rel_path
|
|
113
89
|
remote_etag = remote_objects[rel_path].etag
|
|
@@ -118,12 +94,15 @@ class S3:
|
|
|
118
94
|
logger.info(f"skipped: s3://{self.bucket}/{key}")
|
|
119
95
|
continue
|
|
120
96
|
logger.info(f"download: s3://{self.bucket}/{key} to {local_path}")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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")
|
|
127
106
|
if delete:
|
|
128
107
|
local_files = {k: v for k, v in self._list_local_files(data_dir).items() if not k.endswith('.synced')}
|
|
129
108
|
remote_rel = set(remote_objects)
|
|
@@ -140,10 +119,7 @@ class S3:
|
|
|
140
119
|
|
|
141
120
|
def _client(self):
|
|
142
121
|
session = boto3.Session(profile_name=self.user_profile)
|
|
143
|
-
|
|
144
|
-
# too-small pool (botocore's default is 10) or emit "connection pool is full" warnings.
|
|
145
|
-
config = Config(max_pool_connections=self.max_workers)
|
|
146
|
-
return session.client('s3', config=config)
|
|
122
|
+
return session.client('s3')
|
|
147
123
|
|
|
148
124
|
@staticmethod
|
|
149
125
|
def _normalize_etag(etag):
|
|
@@ -164,39 +140,9 @@ class S3:
|
|
|
164
140
|
with open(local_path, 'rb') as body:
|
|
165
141
|
response = client.put_object(Bucket=self.bucket, Key=key, Body=body)
|
|
166
142
|
return self._normalize_etag(response.get('ETag'))
|
|
167
|
-
client.upload_file(local_path, self.bucket, key
|
|
143
|
+
client.upload_file(local_path, self.bucket, key)
|
|
168
144
|
return self._object_etag(client, key) if need_etag else None
|
|
169
145
|
|
|
170
|
-
def _run_transfers(self, items, transfer):
|
|
171
|
-
# Run transfer(item) for each item across a thread pool, returning the failure count.
|
|
172
|
-
# Per-item ClientError/BotoCoreError are logged and counted (matching the sequential
|
|
173
|
-
# behavior); all logging happens here on the main thread, so the workers do pure I/O.
|
|
174
|
-
if not items:
|
|
175
|
-
return 0
|
|
176
|
-
failures = 0
|
|
177
|
-
with ThreadPoolExecutor(max_workers=min(self.max_workers, len(items))) as pool:
|
|
178
|
-
futures = [pool.submit(transfer, item) for item in items]
|
|
179
|
-
for future in as_completed(futures):
|
|
180
|
-
try:
|
|
181
|
-
future.result()
|
|
182
|
-
except (ClientError, BotoCoreError) as e:
|
|
183
|
-
failures += 1
|
|
184
|
-
logger.info(f"\n*** Error ***\n{e}\n")
|
|
185
|
-
return failures
|
|
186
|
-
|
|
187
|
-
def _push_one(self, client, item, sync_status_dir):
|
|
188
|
-
rel_path, local_path, key = item
|
|
189
|
-
etag = self._upload(client, local_path, key, sync_status_dir is not None)
|
|
190
|
-
if sync_status_dir is not None:
|
|
191
|
-
self._create_sync_marker(rel_path, sync_status_dir, etag)
|
|
192
|
-
|
|
193
|
-
def _pull_one(self, client, item, sync_status_dir):
|
|
194
|
-
rel_path, key, remote_etag, local_path = item
|
|
195
|
-
os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True)
|
|
196
|
-
client.download_file(self.bucket, key, local_path, Config=self._TRANSFER_CONFIG)
|
|
197
|
-
if sync_status_dir is not None:
|
|
198
|
-
self._create_sync_marker(rel_path, sync_status_dir, remote_etag)
|
|
199
|
-
|
|
200
146
|
def _normalize_prefix(self, s3_path):
|
|
201
147
|
# Returns '' for falsy input so callers can concatenate key segments without a leading slash.
|
|
202
148
|
if not s3_path:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|