Skip to content

Commit 4d82515

Browse files
committed
Satisfy linter
1 parent 3a41c5c commit 4d82515

File tree

4 files changed

+122
-54
lines changed

4 files changed

+122
-54
lines changed

generate-address-list.py

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,109 @@
11
#!/usr/bin/env python3
22

3-
import xml.etree.ElementTree as ET
43
import argparse
5-
import pathlib
6-
import json
74
import hashlib
5+
import json
6+
import pathlib
7+
import xml.etree.ElementTree as ET
8+
89
from ofac_scraper import OfacWebsiteScraper
910

1011
FEATURE_TYPE_TEXT = "Digital Currency Address - "
11-
NAMESPACE = {'sdn': 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/ADVANCED_XML'}
12+
NAMESPACE = {
13+
'sdn': 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/'
14+
'ADVANCED_XML'
15+
}
1216

1317
# List of implemented output formats
1418
OUTPUT_FORMATS = ["TXT", "JSON"]
1519

1620
SDN_ADVANCED_FILE_PATH = "sdn_advanced.xml"
1721

22+
1823
def parse_arguments():
1924
parser = argparse.ArgumentParser(
20-
description='Tool to extract sanctioned digital currency addresses from the OFAC special designated nationals XML file (sdn_advanced.xml)')
21-
parser.add_argument('assets', nargs='*',
22-
default=[], help='the asset for which the sanctioned addresses should be extracted (default: XBT (Bitcoin))')
23-
parser.add_argument('-sdn', '--special-designated-nationals-list', dest='sdn', type=argparse.FileType('rb'),
24-
help='the path to the sdn_advanced.xml file (can be downloaded from https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml)', default=SDN_ADVANCED_FILE_PATH)
25-
parser.add_argument('-f', '--output-format', dest='format', nargs='*', choices=OUTPUT_FORMATS,
26-
default=OUTPUT_FORMATS[0], help='the output file format of the address list (default: TXT)')
27-
parser.add_argument('-path', '--output-path', dest='outpath', type=pathlib.Path, default=pathlib.Path(
28-
"./"), help='the path where the lists should be written to (default: current working directory ("./")')
25+
description='Tool to extract sanctioned digital currency addresses from the '
26+
'OFAC special designated nationals XML file (sdn_advanced.xml)')
27+
parser.add_argument(
28+
'assets',
29+
nargs='*',
30+
default=[],
31+
help='the asset for which the sanctioned addresses should be extracted '
32+
'(default: XBT (Bitcoin))'
33+
)
34+
parser.add_argument(
35+
'-sdn',
36+
'--special-designated-nationals-list',
37+
dest='sdn',
38+
type=argparse.FileType('rb'),
39+
help='the path to the sdn_advanced.xml file (can be downloaded from '
40+
'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml)',
41+
default=SDN_ADVANCED_FILE_PATH
42+
)
43+
parser.add_argument(
44+
'-f',
45+
'--output-format',
46+
dest='format',
47+
nargs='*',
48+
choices=OUTPUT_FORMATS,
49+
default=OUTPUT_FORMATS[0],
50+
help='the output file format of the address list (default: TXT)'
51+
)
52+
parser.add_argument(
53+
'-path',
54+
'--output-path',
55+
dest='outpath',
56+
type=pathlib.Path,
57+
default=pathlib.Path("./"),
58+
help='the path where the lists should be written to (default: current working '
59+
'directory ("./")'
60+
)
2961
return parser.parse_args()
3062

63+
3164
def feature_type_text(asset):
3265
"""returns text we expect in a <FeatureType></FeatureType> tag for a given asset"""
3366
return "Digital Currency Address - " + asset
3467

68+
3569
def get_possible_assets(root):
3670
"""
3771
Returns a list of possible digital currency assets from the parsed XML.
3872
"""
3973
assets = []
40-
feature_types = root.findall('sdn:ReferenceValueSets/sdn:FeatureTypeValues/sdn:FeatureType', NAMESPACE)
74+
feature_types = root.findall(
75+
'sdn:ReferenceValueSets/sdn:FeatureTypeValues/sdn:FeatureType',
76+
NAMESPACE
77+
)
4178
for feature_type in feature_types:
4279
if feature_type.text.startswith('Digital Currency Address - '):
4380
asset = feature_type.text.replace('Digital Currency Address - ', '')
4481
assets.append(asset)
4582
return assets
4683

84+
4785
def get_address_id(root, asset):
4886
"""returns the feature id of the given asset"""
4987
feature_type = root.find(
50-
"sdn:ReferenceValueSets/sdn:FeatureTypeValues/*[.='{}']".format(feature_type_text(asset)), NAMESPACE)
51-
if feature_type == None:
52-
raise LookupError("No FeatureType with the name {} found".format(
53-
feature_type_text(asset)))
88+
f"sdn:ReferenceValueSets/sdn:FeatureTypeValues"
89+
f"/*[.='{feature_type_text(asset)}']",
90+
NAMESPACE
91+
)
92+
if feature_type is None:
93+
raise LookupError(
94+
f"No FeatureType with the name {feature_type_text(asset)} found"
95+
)
5496
address_id = feature_type.attrib["ID"]
5597
return address_id
5698

5799

58100
def get_sanctioned_addresses(root, address_id):
59101
"""returns a list of sanctioned addresses for the given address_id"""
60102
addresses = list()
61-
for feature in root.findall("sdn:DistinctParties//*[@FeatureTypeID='{}']".format(address_id), NAMESPACE):
103+
for feature in root.findall(
104+
f"sdn:DistinctParties//*[@FeatureTypeID='{address_id}']",
105+
NAMESPACE
106+
):
62107
for version_detail in feature.findall(".//sdn:VersionDetail", NAMESPACE):
63108
addresses.append(version_detail.text)
64109
return addresses
@@ -72,14 +117,15 @@ def write_addresses(addresses, asset, output_formats, outpath):
72117

73118

74119
def write_addresses_txt(addresses, asset, outpath):
75-
with open("{}/sanctioned_addresses_{}.txt".format(outpath, asset), 'w') as out:
120+
with open(f"{outpath}/sanctioned_addresses_{asset}.txt", 'w') as out:
76121
for address in addresses:
77-
out.write(address+"\n")
122+
out.write(address + "\n")
78123

79124

80125
def write_addresses_json(addresses, asset, outpath):
81-
with open("{}/sanctioned_addresses_{}.json".format(outpath, asset), 'w') as out:
82-
out.write(json.dumps(addresses, indent=2)+"\n")
126+
with open(f"{outpath}/sanctioned_addresses_{asset}.json", 'w') as out:
127+
out.write(json.dumps(addresses, indent=2) + "\n")
128+
83129

84130
def compute_sha256(file_path):
85131
sha256_hash = hashlib.sha256()
@@ -88,10 +134,12 @@ def compute_sha256(file_path):
88134
sha256_hash.update(chunk)
89135
return sha256_hash.hexdigest()
90136

137+
91138
def write_checksum_file(sha256, checksum_file_path):
92139
with open(checksum_file_path, "w") as checksum_file:
93140
checksum_file.write(f"SHA256({SDN_ADVANCED_FILE_PATH}) = {sha256}\n")
94141

142+
95143
def main():
96144
args = parse_arguments()
97145

@@ -110,7 +158,7 @@ def main():
110158
root = tree.getroot()
111159

112160
assets = list()
113-
if type(args.assets) == str:
161+
if isinstance(args.format, str):
114162
assets.append(args.assets)
115163
else:
116164
assets = args.assets
@@ -119,7 +167,7 @@ def main():
119167
assets = get_possible_assets(root)
120168

121169
output_formats = list()
122-
if type(args.format) == str:
170+
if isinstance(args.format, str):
123171
output_formats.append(args.format)
124172
else:
125173
output_formats = args.format
@@ -150,5 +198,6 @@ def main():
150198

151199
write_checksum_file(sha256_checksum_from_site, "data/sdn_advanced_checksum.txt")
152200

201+
153202
if __name__ == "__main__":
154203
main()

ofac_scraper.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import logging
21
import time
2+
33
from selenium import webdriver
44
from selenium.common.exceptions import TimeoutException
5-
from selenium.webdriver.chrome.service import Service
65
from selenium.webdriver.chrome.options import Options
6+
from selenium.webdriver.chrome.service import Service
77
from selenium.webdriver.common.action_chains import ActionChains
88
from selenium.webdriver.common.by import By
9-
from selenium.webdriver.support.ui import WebDriverWait
109
from selenium.webdriver.support import expected_conditions as EC
11-
from webdriver_manager.chrome import ChromeDriverManager
10+
from selenium.webdriver.support.ui import WebDriverWait
1211

1312

1413
class OfacWebsiteScraper:
@@ -41,7 +40,8 @@ def get_sha256_checksum(self):
4140

4241
for attempt in range(MAX_RETRIES):
4342
print(
44-
f"Attempting to get SHA-256 checksum (attempt {attempt + 1}/{MAX_RETRIES})..."
43+
f"Attempting to get SHA-256 checksum (attempt"
44+
f" {attempt + 1}/{MAX_RETRIES})..."
4545
)
4646
try:
4747
self.open_website("https://sanctionslist.ofac.treas.gov/Home/SdnList")
@@ -53,7 +53,10 @@ def get_sha256_checksum(self):
5353

5454
# Scroll to (waiting for animation) and Click the 'File
5555
# Signatures' button with the known ID
56-
header_element = self.driver.find_element(By.ID, "accordion__heading-:r1:")
56+
header_element = self.driver.find_element(
57+
By.ID,
58+
"accordion__heading-:r1:"
59+
)
5760
ActionChains(self.driver).move_to_element(header_element).perform()
5861
time.sleep(1)
5962
header_element.click()
@@ -78,7 +81,8 @@ def get_sha256_checksum(self):
7881

7982
except TimeoutException:
8083
print(
81-
f"Timeout occurred on attempt {attempt + 1}/{MAX_RETRIES}. Retrying in {RETRY_DELAY} seconds..."
84+
f"Timeout occurred on attempt {attempt + 1}/{MAX_RETRIES}. "
85+
f"Retrying in {RETRY_DELAY} seconds..."
8286
)
8387
time.sleep(RETRY_DELAY)
8488
if attempt == MAX_RETRIES - 1:

update_s3_objects.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
#!/usr/bin/env python3
22

33
import argparse
4+
import base64
45
import concurrent.futures
56
import glob
7+
import json
68
import logging
79
import os
8-
import json
910
import threading
1011
from io import BytesIO
11-
import base64
12-
from typing import List
1312

1413
import boto3
1514
from botocore.exceptions import ClientError
@@ -34,7 +33,8 @@
3433
def parse_arguments():
3534
"""Parse command line arguments."""
3635
parser = argparse.ArgumentParser(
37-
description='Read sanctioned addresses from TXT files and create/remove empty S3 objects to match the SDN'
36+
description='Read sanctioned addresses from TXT files and create/remove empty '
37+
'S3 objects to match the SDN'
3838
)
3939
parser.add_argument(
4040
'-d', '--directory',
@@ -123,7 +123,10 @@ def read_sanctioned_addresses(directory):
123123
try:
124124
with open(file_path) as f:
125125
addresses = [line.strip() for line in f if line.strip()]
126-
logger.info(f"Read {len(addresses)} addresses from {os.path.basename(file_path)}")
126+
logger.info(
127+
f"Read {len(addresses)} addresses from "
128+
f"{os.path.basename(file_path)}"
129+
)
127130
unique_addresses.update(addresses)
128131
except Exception as e:
129132
logger.error(f"Error reading {file_path}: {e}")
@@ -145,7 +148,9 @@ def create_s3_object(address, bucket, prefix, dry_run, s3_client):
145148
object_key = f"{prefix}{encode(address)}"
146149

147150
if dry_run:
148-
logger.info(f"DRY RUN: Would create S3 object s3://{bucket}/{object_key} ({address})")
151+
logger.info(
152+
f"DRY RUN: Would create S3 object s3://{bucket}/{object_key} "
153+
f"({address})")
149154
return True, None
150155

151156
try:
@@ -167,7 +172,9 @@ def delete_s3_object(address, bucket, prefix, dry_run, s3_client):
167172
"""Delete an S3 object for the given address."""
168173
object_key = f"{prefix}{encode(address)}"
169174
if dry_run:
170-
logger.info(f"DRY RUN: Would delete S3 object s3://{bucket}/{object_key} ({address})")
175+
logger.info(
176+
f"DRY RUN: Would delete S3 object s3://{bucket}/{object_key} ({address})"
177+
)
171178
return True, None
172179
try:
173180
s3_client.delete_object(
@@ -177,7 +184,8 @@ def delete_s3_object(address, bucket, prefix, dry_run, s3_client):
177184
# Check if the object was actually deleted
178185
try:
179186
s3_client.head_object(Bucket=bucket, Key=object_key)
180-
return False, f"Failed to delete S3 object for {address}: Object still exists"
187+
return False, f"Failed to delete S3 object for {address}: "
188+
"Object still exists"
181189
except ClientError as e:
182190
if e.response['Error']['Code'] == '404':
183191
# 404. File is gone
@@ -208,7 +216,8 @@ def process_action_chunk(action_chunk, bucket, prefix, dry_run, s3_client):
208216
results['created'] += 1
209217
else:
210218
results['errors'] += 1
211-
if error: logger.error(error)
219+
if error:
220+
logger.error(error)
212221
case 'remove':
213222
success, error = delete_s3_object(
214223
action['address'], bucket, prefix, dry_run, s3_client
@@ -239,10 +248,14 @@ def reconcile_s3(
239248
action_list = list(actions)
240249
total_actions = len(action_list)
241250

242-
logger.info(f"Starting to take {total_actions} actions on S3 objects using {workers} worker threads")
251+
logger.info(f"Starting to take {total_actions} actions on S3 objects using "
252+
f"{workers} worker threads")
243253

244254
# Create chunks of actions to process
245-
action_chunks = [action_list[i:i + chunk_size] for i in range(0, total_actions, chunk_size)]
255+
action_chunks = [
256+
action_list[i:i + chunk_size]
257+
for i in range(0, len(action_list), chunk_size)
258+
]
246259

247260
created_count = 0
248261
removed_count = 0
@@ -267,9 +280,11 @@ def reconcile_s3(
267280
error_count += results['errors']
268281

269282
logger.info(
270-
f"Completed chunk {chunk_index+1}/{len(action_chunks)}, "
271-
f"total progress: {created_count + removed_count + error_count}/{total_actions} "
272-
f"({(created_count + removed_count + error_count) / total_actions * 100:.1f}%)"
283+
f"Completed chunk {chunk_index + 1}/{len(action_chunks)}, "
284+
f"total progress: {created_count + removed_count + error_count}/"
285+
f"{total_actions} ({(
286+
created_count + removed_count + error_count
287+
) / total_actions * 100:.1f}%)"
273288
)
274289

275290
except Exception as e:

update_s3_objects_test.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22
Test S3 interaction
33
"""
44
import base64
5-
import unittest
6-
from unittest.mock import Mock, patch
7-
from typing import List
85
import tempfile
6+
import unittest
97
from pathlib import Path
8+
from unittest.mock import Mock, patch
9+
1010
from botocore.exceptions import ClientError
1111

1212
from update_s3_objects import (
13+
create_s3_object,
1314
decode,
15+
delete_s3_object,
1416
encode,
17+
format_result_message,
1518
generate_actions,
16-
read_sanctioned_addresses,
1719
process_action_chunk,
18-
create_s3_object,
19-
delete_s3_object,
20-
format_result_message
20+
read_sanctioned_addresses,
2121
)
2222

2323

@@ -89,7 +89,7 @@ def test_roundtrip(self) -> None:
8989

9090
def test_padding_handling(self) -> None:
9191
"""Test that the functions correctly handle padding."""
92-
test_cases: List[str] = []
92+
test_cases: list[str] = []
9393
test_cases.append("abc") # 3 bytes = 4 base64 chars, no padding
9494
test_cases.append("abcd") # 4 bytes = 6 base64 chars, 2 padding chars
9595
test_cases.append("abcde") # 5 bytes = 8 base64 chars, 1 padding char

0 commit comments

Comments
 (0)