Skip to content

Commit e0ceffb

Browse files
authored
Merge pull request #27 from brave-intl/fix/formatting
Fix formatting
2 parents e5ff6aa + e41d0d9 commit e0ceffb

File tree

6 files changed

+343
-238
lines changed

6 files changed

+343
-238
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
__pycache__/
33
*.py[cod]
44
*$py.class
5+
poetry.lock
56

67
# C extensions
78
*.so

generate-address-list.py

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,108 @@
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+
)
28+
parser.add_argument(
29+
"assets",
30+
nargs="*",
31+
default=[],
32+
help="the asset for which the sanctioned addresses should be extracted "
33+
"(default: XBT (Bitcoin))",
34+
)
35+
parser.add_argument(
36+
"-sdn",
37+
"--special-designated-nationals-list",
38+
dest="sdn",
39+
type=argparse.FileType("rb"),
40+
help="the path to the sdn_advanced.xml file (can be downloaded from "
41+
"https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml)",
42+
default=SDN_ADVANCED_FILE_PATH,
43+
)
44+
parser.add_argument(
45+
"-f",
46+
"--output-format",
47+
dest="format",
48+
nargs="*",
49+
choices=OUTPUT_FORMATS,
50+
default=OUTPUT_FORMATS[0],
51+
help="the output file format of the address list (default: TXT)",
52+
)
53+
parser.add_argument(
54+
"-path",
55+
"--output-path",
56+
dest="outpath",
57+
type=pathlib.Path,
58+
default=pathlib.Path("./"),
59+
help="the path where the lists should be written to (default: current working "
60+
'directory ("./")',
61+
)
2962
return parser.parse_args()
3063

64+
3165
def feature_type_text(asset):
3266
"""returns text we expect in a <FeatureType></FeatureType> tag for a given asset"""
3367
return "Digital Currency Address - " + asset
3468

69+
3570
def get_possible_assets(root):
3671
"""
3772
Returns a list of possible digital currency assets from the parsed XML.
3873
"""
3974
assets = []
40-
feature_types = root.findall('sdn:ReferenceValueSets/sdn:FeatureTypeValues/sdn:FeatureType', NAMESPACE)
75+
feature_types = root.findall(
76+
"sdn:ReferenceValueSets/sdn:FeatureTypeValues/sdn:FeatureType", NAMESPACE
77+
)
4178
for feature_type in feature_types:
42-
if feature_type.text.startswith('Digital Currency Address - '):
43-
asset = feature_type.text.replace('Digital Currency Address - ', '')
79+
if feature_type.text.startswith("Digital Currency Address - "):
80+
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"""
60-
addresses = list()
61-
for feature in root.findall("sdn:DistinctParties//*[@FeatureTypeID='{}']".format(address_id), NAMESPACE):
102+
addresses = []
103+
for feature in root.findall(
104+
f"sdn:DistinctParties//*[@FeatureTypeID='{address_id}']", NAMESPACE
105+
):
62106
for version_detail in feature.findall(".//sdn:VersionDetail", NAMESPACE):
63107
addresses.append(version_detail.text)
64108
return addresses
@@ -72,14 +116,15 @@ def write_addresses(addresses, asset, output_formats, outpath):
72116

73117

74118
def write_addresses_txt(addresses, asset, outpath):
75-
with open("{}/sanctioned_addresses_{}.txt".format(outpath, asset), 'w') as out:
119+
with open(f"{outpath}/sanctioned_addresses_{asset}.txt", "w") as out:
76120
for address in addresses:
77-
out.write(address+"\n")
121+
out.write(address + "\n")
78122

79123

80124
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")
125+
with open(f"{outpath}/sanctioned_addresses_{asset}.json", "w") as out:
126+
out.write(json.dumps(addresses, indent=2) + "\n")
127+
83128

84129
def compute_sha256(file_path):
85130
sha256_hash = hashlib.sha256()
@@ -88,10 +133,12 @@ def compute_sha256(file_path):
88133
sha256_hash.update(chunk)
89134
return sha256_hash.hexdigest()
90135

136+
91137
def write_checksum_file(sha256, checksum_file_path):
92138
with open(checksum_file_path, "w") as checksum_file:
93139
checksum_file.write(f"SHA256({SDN_ADVANCED_FILE_PATH}) = {sha256}\n")
94140

141+
95142
def main():
96143
args = parse_arguments()
97144

@@ -109,17 +156,17 @@ def main():
109156
tree = ET.parse(args.sdn)
110157
root = tree.getroot()
111158

112-
assets = list()
113-
if type(args.assets) == str:
159+
assets = []
160+
if isinstance(args.format, str):
114161
assets.append(args.assets)
115162
else:
116163
assets = args.assets
117164

118165
if len(assets) == 0:
119166
assets = get_possible_assets(root)
120167

121-
output_formats = list()
122-
if type(args.format) == str:
168+
output_formats = []
169+
if isinstance(args.format, str):
123170
output_formats.append(args.format)
124171
else:
125172
output_formats = args.format
@@ -150,5 +197,6 @@ def main():
150197

151198
write_checksum_file(sha256_checksum_from_site, "data/sdn_advanced_checksum.txt")
152199

200+
153201
if __name__ == "__main__":
154202
main()

ofac_scraper.py

Lines changed: 18 additions & 15 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 import expected_conditions as ec
910
from selenium.webdriver.support.ui import WebDriverWait
10-
from selenium.webdriver.support import expected_conditions as EC
11-
from webdriver_manager.chrome import ChromeDriverManager
1211

1312

1413
class OfacWebsiteScraper:
@@ -29,19 +28,20 @@ def open_website(self, url):
2928

3029
def wait_for_element(self, by, value, timeout=30):
3130
return WebDriverWait(self.driver, timeout).until(
32-
EC.presence_of_element_located((by, value))
31+
ec.presence_of_element_located((by, value))
3332
)
3433

3534
def get_element_text(self, by, value):
3635
return self.driver.find_element(by, value).text
3736

3837
def get_sha256_checksum(self):
39-
MAX_RETRIES = 10
40-
RETRY_DELAY = 10 # seconds to wait before retrying
38+
max_retries = 10
39+
retry_delay = 10
4140

42-
for attempt in range(MAX_RETRIES):
41+
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,9 @@ 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, "accordion__heading-:r1:"
58+
)
5759
ActionChains(self.driver).move_to_element(header_element).perform()
5860
time.sleep(1)
5961
header_element.click()
@@ -76,15 +78,16 @@ def get_sha256_checksum(self):
7678
sha256_checksum = checksums_content.split("SHA-256: ")[1].split("\n")[0]
7779
return sha256_checksum
7880

79-
except TimeoutException:
81+
except TimeoutException as e:
8082
print(
81-
f"Timeout occurred on attempt {attempt + 1}/{MAX_RETRIES}. Retrying in {RETRY_DELAY} seconds..."
83+
f"Timeout occurred on attempt {attempt + 1}/{max_retries}. "
84+
f"Retrying in {retry_delay} seconds..."
8285
)
83-
time.sleep(RETRY_DELAY)
84-
if attempt == MAX_RETRIES - 1:
86+
time.sleep(retry_delay)
87+
if attempt == max_retries - 1:
8588
raise TimeoutException(
8689
"Max retries reached. The website is not responding."
87-
)
90+
) from e
8891

8992
def close(self):
9093
self.driver.quit()

pyproject.toml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
[tool.poetry]
2+
name = "ofac-sanctions-list"
3+
version = "0.1.0"
4+
description = "Extract and manage OFAC sanctioned digital currency addresses"
5+
readme = "README.md"
6+
packages = [
7+
{include = "ofac_address_extractor.py"},
8+
{include = "update_s3_objects.py"},
9+
{include = "ofac_scraper.py"}
10+
]
11+
12+
[tool.poetry.scripts]
13+
ofac-extract = "ofac_address_extractor:main"
14+
ofac-s3-sync = "update_s3_objects:main"
15+
16+
[tool.poetry.dependencies]
17+
python = "^3.11"
18+
boto3 = "^1.40.73"
19+
botocore = "^1.35.73"
20+
21+
[tool.poetry.group.dev.dependencies]
22+
pytest = "8.4.2"
23+
pytest-cov = "4.1.0"
24+
mypy = "1.18.2"
25+
ruff = "0.8.6"
26+
bandit = {extras = ["toml"], version = "1.8.6"}
27+
pre-commit = "4.3.0"
28+
vulture = "2.14"
29+
types-boto3 = "^1.0.2"
30+
boto3-stubs = {extras = ["s3"], version = "^1.40.73"}
31+
32+
[tool.ruff]
33+
line-length = 88
34+
target-version = "py311"
35+
36+
[tool.ruff.lint]
37+
select = ["E", "F", "I", "N", "UP", "B", "A", "C4"]
38+
ignore = []
39+
40+
[tool.mypy]
41+
python_version = "3.11"
42+
warn_return_any = true
43+
warn_unused_configs = true
44+
disallow_untyped_defs = true
45+
46+
[[tool.mypy.overrides]]
47+
module = ["boto3.*", "botocore.*"]
48+
ignore_missing_imports = true
49+
50+
[tool.bandit]
51+
exclude_dirs = ["tests", "tmp", "data"]
52+
skips = ["B404", "B603", "B607"]
53+
54+
[tool.pytest.ini_options]
55+
testpaths = ["."]
56+
python_files = ["test_*.py"]
57+
58+
[tool.vulture]
59+
exclude = ["tests/", "tmp/", "data/"]
60+
min_confidence = 80
61+
62+
[build-system]
63+
requires = ["poetry-core"]
64+
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)