Skip to content

Commit 252bc4f

Browse files
authored
Merge pull request #75 from MITLibraries/IR-206-subtasks
IR 206 subtasks - IR 231 and IR 232
2 parents 7aa8b80 + bf93724 commit 252bc4f

3 files changed

Lines changed: 91 additions & 50 deletions

File tree

lambdas/aip.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,18 +243,18 @@ def _check_aip_files_match_manifest(self) -> None:
243243
if missing_in_aip:
244244
missing_files = list(missing_in_aip)
245245
raise AIPValidationError(
246-
"Files found in manifest but missing from AIP",
246+
"File(s) missing from AIP that are present in manifest",
247247
error_details={
248-
"type": "files_missing_in_aip",
248+
"type": "files_missing_from_aip",
249249
"missing_files": missing_files,
250250
},
251251
)
252252
if missing_in_manifest:
253253
missing_files = list(missing_in_manifest)
254254
raise AIPValidationError(
255-
"Files found in AIP but missing from manifest",
255+
"Unexpected file(s) in AIP that are not present in manifest",
256256
error_details={
257-
"type": "files_missing_in_manifest",
257+
"type": "unexpected_files_in_aip",
258258
"missing_files": missing_files,
259259
},
260260
)

lambdas/utils/aws/s3_inventory.py

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,17 @@ def get_aips_df(self) -> pd.DataFrame:
169169
for that AIP. To do this, the AIP UUID -- which is minted in Archivematica -- is
170170
extracted from the S3 key.
171171
172-
Example s3 key for an AIP:
172+
Example s3 prefix for an AIP:
173173
5b33/1bf3/eb1f/4017/bbe8/c24a/9f60/f4cd/2014_039_002-5b331bf3-eb1f-4017-bbe8-c24a9f60f4cd
174174
175175
Where the UUID can be seen as both part of the leading pairtree path and at the
176176
end of the key:
177177
5b331bf3-eb1f-4017-bbe8-c24a9f60f4cd
178178
179-
The following regex is used in this DuckDB SQL query to find a valid UUID in the
180-
S3 key:
181-
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
179+
A regular expression is used to extract AIP UUIDs from S3 keys by locating the
180+
last valid UUID before files like `/bagit.txt` or `/bag-info.txt`. There is some
181+
room for false positives here, but other checks confirm the validity of bags after
182+
a UUID + prefix key is identified from this Inventory data.
182183
183184
The dataframe includes information such as:
184185
- AIP UUID
@@ -190,47 +191,78 @@ def get_aips_df(self) -> pd.DataFrame:
190191
if self._aips_df is not None:
191192
return self._aips_df
192193

193-
# ruff: noqa: E501
194+
aip_regex = (
195+
"""(.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))/(.*)"""
196+
)
197+
# ruff: noqa: E501, UP032
194198
query = """
195-
with cdps_aip_inventory as (
196-
select * from inventory
197-
where is_latest
198-
and not is_delete_marker
199-
),
200-
cdps_aip_inventory_with_aip_uuid as (
201-
select
202-
bucket,
203-
-- extract the *first* UUID encountered in the key
204-
case
205-
when key ~ '.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*'
206-
then regexp_extract(key, '.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*', 1)
207-
else null
208-
end as aip_uuid,
209-
-- extract the inventory root as they key up until, and inclusive, of the first UUID
210-
case
211-
when key ~ '.+?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*'
212-
then regexp_extract(key, '(.+?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*', 1)
213-
else null
214-
end as aip_s3_key,
215-
key,
216-
size,
217-
last_modified_date
218-
from cdps_aip_inventory
199+
-- CTE of all inventory data rows
200+
with cdps_aip_inventory as (
201+
select * from inventory
202+
where is_latest
203+
and not is_delete_marker
204+
),
205+
-- CTE that attempts to extract UUID and AIP prefix from S3 object keys
206+
cdps_aip_inventory_with_aip_uuid as (
207+
select
208+
bucket,
209+
-- the 2nd group in the regex is the AIP UUID
210+
case
211+
when key ~ '{aip_regex}'
212+
then regexp_extract(key, '{aip_regex}', 2)
213+
end as aip_uuid,
214+
-- the 1st group in the regex match is the S3 prefix up until,
215+
-- and including, the AIP UUID
216+
case
217+
when key ~ '{aip_regex}'
218+
then regexp_extract(key, '{aip_regex}', 1)
219+
end as aip_s3_key,
220+
-- the 3rd group is any file suffix after the AIP UUID
221+
case
222+
when key ~ '{aip_regex}'
223+
then regexp_extract(key, '{aip_regex}', 3)
224+
end as aip_suffix,
225+
key,
226+
size,
227+
last_modified_date
228+
from cdps_aip_inventory
229+
),
230+
-- CTE that groups AIPs by UUID and prefix
231+
aips as (
232+
select
233+
bucket,
234+
aip_uuid,
235+
aip_s3_key,
236+
concat('s3://', bucket, '/', aip_s3_key) as aip_s3_uri,
237+
count(*) as aip_files_count,
238+
sum(size) as total_size_bytes,
239+
min(last_modified_date) as earliest_file_date,
240+
max(last_modified_date) as latest_file_date,
241+
list(aip_suffix)::json as aip_file_keys
242+
from cdps_aip_inventory_with_aip_uuid
243+
where aip_uuid is not null
244+
group by bucket, aip_uuid, aip_s3_key, aip_s3_uri
245+
),
246+
-- CTE that limits to what appear to be valid Bagit AIPs (has 'bagit.txt')
247+
bagit_aips as (
248+
select
249+
bucket,
250+
aip_uuid,
251+
aip_s3_key,
252+
aip_s3_uri,
253+
aip_files_count,
254+
total_size_bytes,
255+
earliest_file_date,
256+
latest_file_date
257+
from aips
258+
where 'bagit.txt' in aip_file_keys
259+
)
260+
select * from bagit_aips
261+
order by aip_files_count desc
262+
;
263+
""".format(
264+
aip_regex=aip_regex
219265
)
220-
select
221-
bucket,
222-
aip_uuid,
223-
aip_s3_key,
224-
concat('s3://', bucket, '/', aip_s3_key) as aip_s3_uri,
225-
count(*) as aip_files_count,
226-
sum(size) as total_size_bytes,
227-
min(last_modified_date) as earliest_file_date,
228-
max(last_modified_date) as latest_file_date
229-
from cdps_aip_inventory_with_aip_uuid
230-
where aip_uuid is not null
231-
group by bucket, aip_uuid, aip_s3_key, aip_s3_uri
232-
order by aip_files_count desc;
233-
"""
234266
# ruff: enable: E501
235267

236268
aips_df = self.query_inventory(query)
@@ -261,9 +293,16 @@ def get_aip_from_uuid(self, aip_uuid: str) -> pd.Series:
261293
"""Retrieve information about a specific AIP by its UUID."""
262294
aips_df = self.get_aips_df()
263295

296+
# AIP UUID not found in Inventory data associated with a valid Bagit structure
264297
if aip_uuid not in aips_df["aip_uuid"].to_numpy():
265-
raise ValueError(f"AIP UUID '{aip_uuid}' not found in S3 Inventory data")
298+
raise ValueError(
299+
f"AIP UUID '{aip_uuid}' not found in S3 Inventory data "
300+
"or not associated with a valid Bagit AIP"
301+
)
302+
266303
aip = aips_df.set_index("aip_uuid").loc[aip_uuid]
304+
305+
# AIP UUID found associated with multiple S3 prefixes (e.g. multiple buckets)
267306
if isinstance(aip, pd.DataFrame):
268307
raise TypeError(
269308
f"Multiple entries found for AIP UUID '{aip_uuid}'in S3 Inventory data"

tests/test_aip.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ def test_check_aip_files_match_manifest_missing_files(
192192
with pytest.raises(AIPValidationError) as exc:
193193
aip._check_aip_files_match_manifest()
194194

195-
assert "Files found in manifest but missing from AIP" in str(exc.value)
195+
assert "File(s) missing from AIP" in str(exc.value)
196+
assert exc.value.error_details["type"] == "files_missing_from_aip"
196197

197198
def test_check_aip_files_match_manifest_extra_files(
198199
self, mock_aip_folder, mock_manifest_data, aip
@@ -210,7 +211,8 @@ def test_check_aip_files_match_manifest_extra_files(
210211
with pytest.raises(AIPValidationError) as exc:
211212
aip._check_aip_files_match_manifest()
212213

213-
assert "Files found in AIP but missing from manifest" in str(exc.value)
214+
assert "Unexpected file(s) in AIP" in str(exc.value)
215+
assert exc.value.error_details["type"] == "unexpected_files_in_aip"
214216

215217
def test_check_checksums_mismatch(self, aip):
216218
aip.manifest_df = pd.DataFrame(

0 commit comments

Comments
 (0)