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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: datakit-data
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A datakit plugin to manage data assets on AWS S3.
5
5
  Author: Larry Fenn
6
6
  Author-email: Larry Fenn <lfenn@ap.org>
@@ -1,6 +1,6 @@
1
1
  __author__ = """Larry Fenn"""
2
2
  __email__ = 'lfenn@ap.org'
3
- __version__ = '0.6.0'
3
+ __version__ = '0.6.2'
4
4
 
5
5
  from .commands.init import Init
6
6
  from .commands.push import Push
@@ -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, max_workers=self.project_configs.get('max_workers'))
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, max_workers=self.project_configs.get('max_workers'))
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
- # Per-file transfers (push uploads, pull downloads) are network-latency-bound, so we run them
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
- to_upload.append((rel_path, local_path, key))
70
- if not dryrun:
71
- failures += self._run_transfers(
72
- to_upload,
73
- lambda item: self._push_one(client, item, sync_status_dir),
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
- to_download.append((rel_path, key, remote_etag, local_path))
111
- if not dryrun:
112
- failures += self._run_transfers(
113
- to_download,
114
- lambda item: self._pull_one(client, item, sync_status_dir),
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
- # Size the connection pool to the worker count so concurrent transfers don't queue on a
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:
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "datakit-data"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "A datakit plugin to manage data assets on AWS S3."
9
9
  authors = [
10
10
  {name = "Larry Fenn", email = "lfenn@ap.org"},