1111from botocore .config import Config
1212
1313# Configure logging
14- logging .basicConfig (level = logging .INFO , format = "%(asctime)s [%(levelname)s] %(message)s" )
14+ fmt = "%(asctime)s [%(levelname)s] %(message)s"
15+ logging .basicConfig (level = logging .INFO , format = fmt )
1516logger = logging .getLogger (__name__ )
1617
1718
@@ -34,7 +35,8 @@ def limit(self, pk):
3435 self .history [pk ] = [t for t in self .history [pk ] if now - t < 1.0 ]
3536
3637 if len (self .history [pk ]) >= self .max_rate :
37- # Calculate sleep duration to let the oldest request roll off the 1-second window
38+ # Calculate sleep duration to let the oldest request roll
39+ # off the 1-second window
3840 sleep_time = 1.0 - (now - self .history [pk ][0 ])
3941 if sleep_time > 0 :
4042 time .sleep (sleep_time )
@@ -44,6 +46,53 @@ def limit(self, pk):
4446 self .history [pk ].append (now )
4547
4648
49+ def handle_file (
50+ local_path ,
51+ table ,
52+ rate_limiter : PartitionRateLimiter ,
53+ records_in_file ,
54+ total_records ,
55+ ):
56+ with gzip .open (local_path , "rt" , encoding = "utf-8" ) as f :
57+ with table .batch_writer () as batch :
58+ for line in f :
59+ line = line .strip ()
60+ if not line :
61+ continue
62+
63+ try :
64+ # Convert floats/doubles to Decimal for
65+ # DynamoDB compatibility
66+ item = json .loads (line , parse_float = Decimal )
67+ except Exception as e :
68+ logger .error (f"Failed to parse JSON line: { e } " )
69+ continue
70+
71+ # Add/modify fields if needed
72+ newdate = datetime .datetime .utcnow ().isoformat () + "Z"
73+ item ["imported_at" ] = newdate
74+
75+ # Ensure partition and sort keys are present
76+ pk = item .get ("pk" )
77+ sk = item .get ("sk" )
78+ if not pk or not sk :
79+ logger .warning (
80+ "Skipping record missing pk/sk: %s" ,
81+ item .get ('granule_id' ),
82+ )
83+
84+ continue
85+
86+ # Apply dynamic rate limit based on the partition key
87+ rate_limiter .limit (pk )
88+
89+ # Batch insert into DynamoDB
90+ batch .put_item (Item = item )
91+ records_in_file += 1
92+ total_records += 1
93+ return records_in_file , total_records
94+
95+
4796def main ():
4897 bucket_name = os .environ .get ("CTORM_BUCKET" , "ctorm-scratch" )
4998 table_name = os .environ .get ("TABLE_NAME" )
@@ -55,25 +104,32 @@ def main():
55104
56105 s3 = boto3 .client ("s3" )
57106
58- # Configure boto3 with more aggressive retries to gracefully handle scale peaks
107+ # Configure boto3 with more aggressive retries to gracefully
108+ # handle scale peaks
59109 retry_config = Config (
60110 retries = {
61111 "max_attempts" : 10 ,
62- "mode" : "standard"
112+ "mode" : "standard" ,
63113 }
64114 )
65115 dynamodb = boto3 .resource ("dynamodb" , config = retry_config )
66- table = dynamodb .Table (table_name )
116+ dyndb_table = dynamodb .Table (table_name )
67117
68- logger .info (f"Starting import from s3://{ bucket_name } /{ prefix } into DynamoDB table { table_name } " )
118+ logger .info (
119+ "Starting import from s3://%s/%s into DynamoDB table %s" ,
120+ bucket_name ,
121+ prefix ,
122+ table_name ,
123+ )
69124
70125 paginator = s3 .get_paginator ("list_objects_v2" )
71126 pages = paginator .paginate (Bucket = bucket_name , Prefix = prefix )
72127
73128 total_files = 0
74129 total_records = 0
75130
76- # Initialize partition-level rate limiter set to a safe threshold (850 writes/sec per pk)
131+ # Initialize partition-level rate limiter set to a safe
132+ # threshold (850 writes/sec per pk)
77133 rate_limiter = PartitionRateLimiter (max_rate_per_sec = 850 )
78134
79135 # Walk through the bucket objects
@@ -94,52 +150,42 @@ def main():
94150 try :
95151 s3 .download_file (bucket_name , key , local_path )
96152 except Exception as e :
97- logger .error (f"Failed to download s3://{ bucket_name } /{ key } : { e } " )
153+ logger .error (
154+ "Failed to download s3://{%s}/%s: %s" ,
155+ bucket_name ,
156+ key ,
157+ e ,
158+ )
98159 continue
99160
100161 # Open, decompress and parse
101162 records_in_file = 0
102163 try :
103- with gzip .open (local_path , "rt" , encoding = "utf-8" ) as f :
104- with table .batch_writer () as batch :
105- for line in f :
106- line = line .strip ()
107- if not line :
108- continue
109-
110- try :
111- # Convert floats/doubles to Decimal for DynamoDB compatibility
112- item = json .loads (line , parse_float = Decimal )
113- except Exception as e :
114- logger .error (f"Failed to parse JSON line: { e } " )
115- continue
116-
117- # Add/modify fields if needed
118- item ["imported_at" ] = datetime .datetime .utcnow ().isoformat () + "Z"
119-
120- # Ensure partition and sort keys are present
121- pk = item .get ("pk" )
122- sk = item .get ("sk" )
123- if not pk or not sk :
124- logger .warning (f"Skipping record missing pk/sk: { item .get ('granule_id' )} " )
125- continue
126-
127- # Apply dynamic rate limit based on the partition key
128- rate_limiter .limit (pk )
129-
130- # Batch insert into DynamoDB
131- batch .put_item (Item = item )
132- records_in_file += 1
133- total_records += 1
134-
135- logger .info (f"Successfully imported { records_in_file } records from { key } " )
164+ records_in_file , total_records = handle_file (
165+ local_path ,
166+ dyndb_table ,
167+ rate_limiter ,
168+ records_in_file ,
169+ total_records
170+ )
171+
172+ logger .info (
173+ "Successfully imported %s records from %s" ,
174+ records_in_file ,
175+ key ,
176+
177+ )
136178 except Exception as e :
137- logger .error (f "Error processing file { key } : { e } " )
179+ logger .error ("Error processing file %s: %s" , key , e )
138180 finally :
139181 if os .path .exists (local_path ):
140182 os .remove (local_path )
141183
142- logger .info (f"Import complete. Processed { total_files } files, imported { total_records } total records." )
184+ logger .info (
185+ "Import complete. Processed %s files, imported %s total records." ,
186+ total_files ,
187+ total_records ,
188+ )
143189
144190
145191if __name__ == "__main__" :
0 commit comments