Skip to content

Commit 69564cf

Browse files
committed
Update pre-commit hook and failed unittest
1 parent 414c926 commit 69564cf

6 files changed

Lines changed: 174 additions & 102 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ repos:
5959
hooks:
6060
- id: validate-json-configs
6161
name: Validate dataset JSON configs with Pydantic
62-
entry: python3 aodn_cloud_optimised/bin/generic_cloud_optimised_creation.py --validate-configs aodn_cloud_optimised/config/dataset
62+
entry: python3 aodn_cloud_optimised/bin/pydantic_precommit_hook.py --validate-configs aodn_cloud_optimised/config/dataset
6363
language: system
6464
types: [json]
6565
pass_filenames: false

aodn_cloud_optimised/bin/generic_cloud_optimised_creation.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -409,58 +409,6 @@ def resolve_dataset_config_path(config_arg: str) -> str:
409409
return str(config_path)
410410

411411

412-
def validate_all_configs(config_dir: str, exclude_regex: Optional[str] = None) -> int:
413-
"""Validate all JSON config files in a directory using the DatasetConfig schema.
414-
415-
Args:
416-
config_dir: Path to the directory containing JSON configuration files.
417-
exclude_regex: Optional regex pattern to exclude files.
418-
419-
Returns:
420-
Number of invalid configurations (0 if all valid).
421-
"""
422-
config_path = Path(config_dir)
423-
if not config_path.is_dir():
424-
print(f"❌ Provided path is not a directory: {config_dir}")
425-
return 1
426-
427-
exclude_pattern = re.compile(exclude_regex) if exclude_regex else None
428-
429-
json_files = sorted(
430-
p
431-
for p in config_path.glob("*.json")
432-
if p.is_file() and not (exclude_pattern and exclude_pattern.match(str(p)))
433-
)
434-
435-
if not json_files:
436-
print(f"ℹ️ No JSON files to validate in {config_dir}")
437-
return 0
438-
439-
print(f"🔍 Validating {len(json_files)} config file(s) in {config_dir}")
440-
errors = 0
441-
for json_file in json_files:
442-
try:
443-
with open(json_file, "r") as f:
444-
raw = json.load(f)
445-
DatasetConfig.model_validate(raw)
446-
except ValidationError as e:
447-
print(f"\n❌ Validation failed in: {json_file}")
448-
print("─" * 80)
449-
print(e)
450-
print("─" * 80)
451-
errors += 1
452-
except Exception as e:
453-
print(f"\n❌ Error reading {json_file}: {e}")
454-
errors += 1
455-
456-
if errors > 0:
457-
print(f"\n{errors} configuration file(s) failed validation.")
458-
else:
459-
print("✅ All configurations are valid.")
460-
461-
return errors
462-
463-
464412
def main():
465413
parser = argparse.ArgumentParser(
466414
description="Run cloud-optimised creation using config."
@@ -473,22 +421,9 @@ def main():
473421
type=str,
474422
help='JSON string to override config fields. Example: \'{"run_settings": {"cluster": {"mode": null}, "raise_error": true}}\' ',
475423
)
476-
parser.add_argument(
477-
"--validate-configs",
478-
type=str,
479-
help="Validate all JSON configs in a directory (no processing is done).",
480-
)
481424

482425
args = parser.parse_args()
483426

484-
if args.validate_configs:
485-
# Pre-commit pattern: match full path string
486-
exclude_pattern = r"^.*dataset_template\.json$"
487-
exit_code = validate_all_configs(
488-
args.validate_configs, exclude_regex=exclude_pattern
489-
)
490-
sys.exit(exit_code)
491-
492427
try:
493428
config = load_config_and_validate(args.config)
494429
except ValidationError as e:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import json
4+
import logging
5+
import re
6+
import sys
7+
from pathlib import Path
8+
from typing import Optional
9+
10+
from pydantic import (
11+
ValidationError,
12+
)
13+
14+
from aodn_cloud_optimised.bin.generic_cloud_optimised_creation import DatasetConfig
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
def validate_all_configs(config_dir: str, exclude_regex: Optional[str] = None) -> int:
20+
"""Validate all JSON config files in a directory using the DatasetConfig schema.
21+
22+
Args:
23+
config_dir: Path to the directory containing JSON configuration files.
24+
exclude_regex: Optional regex pattern to exclude files.
25+
26+
Returns:
27+
Number of invalid configurations (0 if all valid).
28+
"""
29+
config_path = Path(config_dir)
30+
if not config_path.is_dir():
31+
print(f"❌ Provided path is not a directory: {config_dir}")
32+
return 1
33+
34+
exclude_pattern = re.compile(exclude_regex) if exclude_regex else None
35+
36+
json_files = sorted(
37+
p
38+
for p in config_path.glob("*.json")
39+
if p.is_file() and not (exclude_pattern and exclude_pattern.match(str(p)))
40+
)
41+
42+
if not json_files:
43+
print(f"ℹ️ No JSON files to validate in {config_dir}")
44+
return 0
45+
46+
print(f"🔍 Validating {len(json_files)} config file(s) in {config_dir}")
47+
errors = 0
48+
for json_file in json_files:
49+
try:
50+
with open(json_file, "r") as f:
51+
raw = json.load(f)
52+
DatasetConfig.model_validate(raw)
53+
except ValidationError as e:
54+
print(f"\n❌ Validation failed in: {json_file}")
55+
print("─" * 80)
56+
print(e)
57+
print("─" * 80)
58+
errors += 1
59+
except Exception as e:
60+
print(f"\n❌ Error reading {json_file}: {e}")
61+
errors += 1
62+
63+
if errors > 0:
64+
print(f"\n{errors} configuration file(s) failed validation.")
65+
else:
66+
print("✅ All configurations are valid.")
67+
68+
return errors
69+
70+
71+
def main():
72+
parser = argparse.ArgumentParser(description="Pydantic validator with pre-commit.")
73+
parser.add_argument(
74+
"--validate-configs",
75+
type=str,
76+
help="Validate all JSON configs in a directory (no processing is done).",
77+
)
78+
79+
args = parser.parse_args()
80+
81+
if args.validate_configs:
82+
# Pre-commit pattern: match full path string
83+
exclude_pattern = r"^.*dataset_template\.json$"
84+
exit_code = validate_all_configs(
85+
args.validate_configs, exclude_regex=exclude_pattern
86+
)
87+
sys.exit(exit_code)
88+
89+
90+
if __name__ == "__main__":
91+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ cloud_optimised_vessel_xbt_delayed_qc = "aodn_cloud_optimised.bin.vessel_xbt_del
153153
cloud_optimised_vessel_xbt_realtime_nonqc = "aodn_cloud_optimised.bin.vessel_xbt_realtime_nonqc:main"
154154
cloud_optimised_wave_buoy_realtime_nonqc = "aodn_cloud_optimised.bin.wave_buoy_realtime_nonqc:main"
155155
generic_cloud_optimised_creation = "aodn_cloud_optimised.bin.generic_cloud_optimised_creation:main"
156+
pydantic_precommit_hook = "aodn_cloud_optimised.bin.pydantic_precommit_hook:main"
156157

157158
#[tool.poetry.include]
158159
#data = ["aodn_cloud_optimised/config/*.json", "aodn_cloud_optimised/config/dataset/*.json"]

test_aodn_cloud_optimised/resources/wave_buoy_realtime_nonqc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
"memory_limit": "4GB"
1818
}
1919
},
20+
"paths": [
21+
{
22+
"s3_uri": "s3://imos-data/Department_of_Transport-Western_Australia/WAVE-BUOYS/REALTIME/",
23+
"filter": []
24+
}
25+
],
2026
"batch_size": 300
2127
},
2228
"metadata_uuid": "b299cdcd-3dee-48aa-abdd-e0fcdbb9cadc",

test_aodn_cloud_optimised/test_bin_generic_error.py

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import json
22
import logging
33
import os
4-
import sys
54
import unittest
65
from io import StringIO
7-
from unittest.mock import patch, MagicMock
6+
from unittest.mock import MagicMock, patch
87

98
import boto3
109
import s3fs
10+
from botocore import UNSIGNED
11+
from botocore.session import get_session
12+
from botocore.client import Config as BotoConfig # avoid conflict with json config
1113
from moto import mock_aws
1214
from moto.moto_server.threaded_moto_server import ThreadedMotoServer
1315

14-
from aodn_cloud_optimised.bin.generic_cloud_optimised_creation import main
15-
from aodn_cloud_optimised.lib.clusterLib import ClusterMode
16+
from aodn_cloud_optimised.bin.generic_cloud_optimised_creation import (
17+
DatasetConfig,
18+
main,
19+
)
20+
from aodn_cloud_optimised.lib.config import load_dataset_config
1621

1722
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
1823

@@ -28,10 +33,10 @@
2833
os.path.join(ROOT_DIR, "resources", file_name) for file_name in filenames
2934
]
3035

36+
# On purpose wrong!
3137
DATASET_CONFIG_NC_ACORN_JSON = os.path.join(
3238
ROOT_DIR,
3339
"resources",
34-
# "radar_TurquoiseCoast_velocity_hourly_averaged_delayed_qc.json",
3540
"wave_buoy_realtime_nonqc.json",
3641
)
3742

@@ -120,41 +125,75 @@ def tearDown(self):
120125
self.server.stop()
121126
del os.environ["RUNNING_UNDER_UNITTEST"]
122127

123-
@patch("argparse.ArgumentParser.parse_args")
124-
def test_main(self, mock_parse_args):
125-
# Prepare mock arguments
126-
mock_parse_args.return_value = MagicMock(
127-
paths=["IMOS/ACORN/gridded_1h-avg-current-map_QC"],
128-
# filters=["TURQ"],
129-
filters=[".nc"],
130-
exclude="FV02",
131-
suffix=".nc",
132-
raise_error=True,
133-
dataset_config=DATASET_CONFIG_NC_ACORN_JSON,
134-
clear_existing_data=True,
135-
force_previous_parquet_deletion=False,
136-
# cluster_mode=ClusterMode.LOCAL,
137-
cluster_mode=ClusterMode.NONE,
138-
optimised_bucket_name=self.BUCKET_OPTIMISED_NAME,
139-
root_prefix_cloud_optimised_path="testing",
140-
bucket_raw="imos-data",
141-
)
128+
def test_main_with_config_and_json_overwrite_fail(self):
129+
dataset_config = load_dataset_config(DATASET_CONFIG_NC_ACORN_JSON)
130+
config_validated = DatasetConfig.model_validate(dataset_config)
131+
132+
with open(DATASET_CONFIG_NC_ACORN_JSON) as f:
133+
raw_json = f.read()
134+
config_validated = DatasetConfig.model_validate_json(raw_json)
135+
136+
def _mock_boto3_client(service_name, *args, **kwargs):
137+
if service_name == "s3":
138+
session = get_session()
139+
return session.create_client(
140+
"s3",
141+
endpoint_url="http://127.0.0.1:5555",
142+
region_name="us-east-1",
143+
config=BotoConfig(signature_version=UNSIGNED),
144+
)
145+
raise NotImplementedError(f"Unhandled boto3 service: {service_name}")
146+
147+
with (
148+
patch(
149+
"aodn_cloud_optimised.bin.generic_cloud_optimised_creation.load_config_and_validate",
150+
new=lambda _: config_validated,
151+
),
152+
patch("argparse.ArgumentParser.parse_args") as mock_parse_args,
153+
patch("sys.exit") as mock_sys_exit,
154+
patch(
155+
"aodn_cloud_optimised.lib.s3Tools.boto3.client", new=_mock_boto3_client
156+
),
157+
):
158+
mock_parse_args.return_value = MagicMock(
159+
config=DATASET_CONFIG_NC_ACORN_JSON,
160+
json_overwrite=json.dumps(
161+
{
162+
"run_settings": {
163+
"cluster": {"mode": None},
164+
"raise_error": True,
165+
"force_previous_parquet_deletion": False,
166+
"clear_existing_data": False,
167+
"paths": [
168+
{
169+
"s3_uri": "s3://imos-data/IMOS/ACORN/gridded_1h-avg-current-map_QC",
170+
"filter": [],
171+
},
172+
],
173+
"optimised_bucket_name": self.BUCKET_OPTIMISED_NAME,
174+
"root_prefix_cloud_optimised_path": self.ROOT_PREFIX_CLOUD_OPTIMISED_PATH,
175+
}
176+
}
177+
),
178+
)
142179

143-
# Capture logs
144-
log_stream = StringIO()
145-
log_handler = logging.StreamHandler(log_stream)
146-
logger = logging.getLogger()
147-
logger.addHandler(log_handler)
180+
log_stream = StringIO()
181+
log_handler = logging.StreamHandler(log_stream)
182+
logger = logging.getLogger()
183+
logger.addHandler(log_handler)
148184

149-
with self.assertRaises(Exception) as context:
150-
main()
185+
with self.assertRaises(Exception) as context:
186+
main()
187+
mock_sys_exit.assert_called_with(0)
151188

152-
# with self.assertRaises(SystemExit) as cm:
153-
# main()
154-
# self.assertEqual(cm.exception.code, 1) # Verify exit code
189+
log_handler.flush()
190+
captured_logs = log_stream.getvalue().strip().split("\n")
155191

156-
# Restore stdout
157-
# sys.stdout = sys.__stdout__
192+
assert any(
193+
"Exception: Error in Cloud Optimised process. Forcing script exit"
194+
in log
195+
for log in captured_logs
196+
)
158197

159198

160199
if __name__ == "__main__":

0 commit comments

Comments
 (0)