Coverage for tests/test_cliCmdQueryDatasets.py: 24%

103 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-07 01:48 -0800

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28"""Unit tests for daf_butler CLI query-datasets command.""" 

29 

30import os 

31import unittest 

32 

33from astropy.table import Table as AstropyTable 

34from numpy import array 

35 

36from lsst.daf.butler import CollectionType, InvalidQueryError, StorageClassFactory, script 

37from lsst.daf.butler.tests import addDatasetType 

38from lsst.daf.butler.tests.utils import ButlerTestHelper, MetricTestRepo, makeTestTempDir, removeTestTempDir 

39from lsst.resources import ResourcePath 

40 

41TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

42 

43 

44def expectedFilesystemDatastoreTables(root: ResourcePath): 

45 """Return the expected table contents.""" 

46 return ( 

47 AstropyTable( 

48 array( 

49 ( 

50 ( 

51 "test_metric_comp.data", 

52 "ingest/run", 

53 "DummyCamComp", 

54 "423", 

55 "R", 

56 "d-r", 

57 root.join( 

58 "ingest/run/test_metric_comp.data/" 

59 "test_metric_comp_v00000423_fDummyCamComp_data.yaml" 

60 ), 

61 ), 

62 ( 

63 "test_metric_comp.data", 

64 "ingest/run", 

65 "DummyCamComp", 

66 "424", 

67 "R", 

68 "d-r", 

69 root.join( 

70 "ingest/run/test_metric_comp.data/" 

71 "test_metric_comp_v00000424_fDummyCamComp_data.yaml" 

72 ), 

73 ), 

74 ) 

75 ), 

76 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

77 ), 

78 AstropyTable( 

79 array( 

80 ( 

81 ( 

82 "test_metric_comp.output", 

83 "ingest/run", 

84 "DummyCamComp", 

85 "423", 

86 "R", 

87 "d-r", 

88 root.join( 

89 "ingest/run/test_metric_comp.output/" 

90 "test_metric_comp_v00000423_fDummyCamComp_output.yaml" 

91 ), 

92 ), 

93 ( 

94 "test_metric_comp.output", 

95 "ingest/run", 

96 "DummyCamComp", 

97 "424", 

98 "R", 

99 "d-r", 

100 root.join( 

101 "ingest/run/test_metric_comp.output/" 

102 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

103 ), 

104 ), 

105 ) 

106 ), 

107 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

108 ), 

109 AstropyTable( 

110 array( 

111 ( 

112 ( 

113 "test_metric_comp.summary", 

114 "ingest/run", 

115 "DummyCamComp", 

116 "423", 

117 "R", 

118 "d-r", 

119 root.join( 

120 "ingest/run/test_metric_comp.summary/" 

121 "test_metric_comp_v00000423_fDummyCamComp_summary.yaml" 

122 ), 

123 ), 

124 ( 

125 "test_metric_comp.summary", 

126 "ingest/run", 

127 "DummyCamComp", 

128 "424", 

129 "R", 

130 "d-r", 

131 root.join( 

132 "ingest/run/test_metric_comp.summary/" 

133 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

134 ), 

135 ), 

136 ) 

137 ), 

138 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

139 ), 

140 ) 

141 

142 

143class QueryDatasetsTest(unittest.TestCase, ButlerTestHelper): 

144 """Test the query-datasets command-line.""" 

145 

146 configFile = os.path.join(TESTDIR, "config/basic/butler.yaml") 

147 storageClassFactory = StorageClassFactory() 

148 

149 @staticmethod 

150 def _queryDatasets( 

151 repo, glob=(), collections=(), where="", find_first=False, show_uri=False, limit=0, order_by=() 

152 ): 

153 query = script.QueryDatasets( 

154 glob, 

155 collections, 

156 where=where, 

157 find_first=find_first, 

158 show_uri=show_uri, 

159 limit=limit, 

160 order_by=order_by, 

161 butler=repo, 

162 ) 

163 return list(query.getTables()) 

164 

165 def setUp(self): 

166 self.testdir = makeTestTempDir(TESTDIR) 

167 self.repoDir = os.path.join(self.testdir, "repo") 

168 

169 def tearDown(self): 

170 removeTestTempDir(self.testdir) 

171 

172 def testChained(self): 

173 testRepo = MetricTestRepo( 

174 self.repoDir, configFile=os.path.join(TESTDIR, "config/basic/butler-chained.yaml") 

175 ) 

176 

177 tables = self._queryDatasets(repo=testRepo.butler, show_uri=True, collections="*", glob="*") 

178 

179 # Want second datastore root since in-memory is ephemeral and 

180 # all the relevant datasets are stored in the second as well as third 

181 # datastore. 

182 roots = testRepo.butler.get_datastore_roots() 

183 datastore_root = roots[testRepo.butler.get_datastore_names()[1]] 

184 

185 self.assertAstropyTablesEqual( 

186 tables, 

187 expectedFilesystemDatastoreTables(datastore_root), 

188 filterColumns=True, 

189 ) 

190 

191 def testShowURI(self): 

192 """Test for expected output with show_uri=True.""" 

193 testRepo = MetricTestRepo(self.repoDir, configFile=self.configFile) 

194 

195 tables = self._queryDatasets(repo=testRepo.butler, show_uri=True, collections="*", glob="*") 

196 

197 roots = testRepo.butler.get_datastore_roots() 

198 datastore_root = list(roots.values())[0] 

199 

200 self.assertAstropyTablesEqual( 

201 tables, expectedFilesystemDatastoreTables(datastore_root), filterColumns=True 

202 ) 

203 

204 def testShowUriNoDisassembly(self): 

205 """Test for expected output with show_uri=True and no disassembly.""" 

206 testRepo = MetricTestRepo( 

207 self.repoDir, 

208 configFile=self.configFile, 

209 storageClassName="StructuredCompositeReadCompNoDisassembly", 

210 ) 

211 

212 tables = self._queryDatasets(repo=testRepo.butler, show_uri=True, collections="*", glob="*") 

213 

214 roots = testRepo.butler.get_datastore_roots() 

215 datastore_root = list(roots.values())[0] 

216 

217 expected = [ 

218 AstropyTable( 

219 array( 

220 ( 

221 ( 

222 "test_metric_comp", 

223 "ingest/run", 

224 "DummyCamComp", 

225 "423", 

226 "R", 

227 "d-r", 

228 datastore_root.join( 

229 "ingest/run/test_metric_comp/test_metric_comp_v00000423_fDummyCamComp.yaml" 

230 ), 

231 ), 

232 ( 

233 "test_metric_comp", 

234 "ingest/run", 

235 "DummyCamComp", 

236 "424", 

237 "R", 

238 "d-r", 

239 datastore_root.join( 

240 "ingest/run/test_metric_comp/test_metric_comp_v00000424_fDummyCamComp.yaml" 

241 ), 

242 ), 

243 ) 

244 ), 

245 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

246 ), 

247 ] 

248 

249 self.assertAstropyTablesEqual(tables, expected, filterColumns=True) 

250 

251 def testNoShowURI(self): 

252 """Test for expected output without show_uri (default is False).""" 

253 testRepo = MetricTestRepo(self.repoDir, configFile=self.configFile) 

254 

255 tables = self._queryDatasets(repo=testRepo.butler, collections="*", glob="*") 

256 

257 expectedTables = ( 

258 AstropyTable( 

259 array( 

260 ( 

261 ("test_metric_comp", "ingest/run", "DummyCamComp", "423", "R", "d-r"), 

262 ("test_metric_comp", "ingest/run", "DummyCamComp", "424", "R", "d-r"), 

263 ) 

264 ), 

265 names=("type", "run", "instrument", "visit", "band", "physical_filter"), 

266 ), 

267 ) 

268 

269 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

270 

271 def testWhere(self): 

272 """Test using the where clause to reduce the number of rows returned by 

273 queryDatasets. 

274 """ 

275 testRepo = MetricTestRepo(self.repoDir, configFile=self.configFile) 

276 

277 for glob in (("*",), ("test_metric_comp",)): 

278 with self.subTest(glob=glob): 

279 tables = self._queryDatasets( 

280 repo=testRepo.butler, 

281 where="instrument='DummyCamComp' AND visit=423", 

282 collections="*", 

283 glob=glob, 

284 ) 

285 

286 expectedTables = ( 

287 AstropyTable( 

288 array(("test_metric_comp", "ingest/run", "DummyCamComp", "423", "R", "d-r")), 

289 names=("type", "run", "instrument", "visit", "band", "physical_filter"), 

290 ), 

291 ) 

292 

293 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

294 

295 with self.assertRaises(InvalidQueryError): 

296 self._queryDatasets(repo=testRepo.butler, collections="*", find_first=True, glob=glob) 

297 

298 def testGlobDatasetType(self): 

299 """Test specifying dataset type.""" 

300 # Create and register an additional DatasetType 

301 testRepo = MetricTestRepo(self.repoDir, configFile=self.configFile) 

302 

303 testRepo.butler.registry.insertDimensionData( 

304 "visit", 

305 { 

306 "instrument": "DummyCamComp", 

307 "id": 425, 

308 "name": "fourtwentyfive", 

309 "physical_filter": "d-r", 

310 "day_obs": 20200101, 

311 }, 

312 ) 

313 

314 datasetType = addDatasetType( 

315 testRepo.butler, "alt_test_metric_comp", ("instrument", "visit"), "StructuredCompositeReadComp" 

316 ) 

317 

318 testRepo.addDataset(dataId={"instrument": "DummyCamComp", "visit": 425}, datasetType=datasetType) 

319 

320 # verify the new dataset type increases the number of tables found: 

321 tables = self._queryDatasets(repo=testRepo.butler, collections="*", glob="*") 

322 

323 expectedTables = ( 

324 AstropyTable( 

325 array(("alt_test_metric_comp", "ingest/run", "DummyCamComp", "425", "R", "d-r")), 

326 names=("type", "run", "instrument", "visit", "band", "physical_filter"), 

327 ), 

328 AstropyTable( 

329 array( 

330 ( 

331 ("test_metric_comp", "ingest/run", "DummyCamComp", "423", "R", "d-r"), 

332 ("test_metric_comp", "ingest/run", "DummyCamComp", "424", "R", "d-r"), 

333 ) 

334 ), 

335 names=("type", "run", "instrument", "visit", "band", "physical_filter"), 

336 ), 

337 ) 

338 

339 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

340 

341 # Dataset type (glob) argument will become mandatory soon 

342 with self.assertWarns(FutureWarning): 

343 self._queryDatasets(repo=testRepo.butler, collections="*") 

344 

345 def test_limit_order(self): 

346 """Test limit and ordering.""" 

347 # Create and register an additional DatasetType 

348 testRepo = MetricTestRepo(self.repoDir, configFile=self.configFile) 

349 

350 with self.assertLogs("lsst.daf.butler.script.queryDatasets", level="WARNING") as cm: 

351 tables = self._queryDatasets( 

352 repo=testRepo.butler, 

353 limit=-1, 

354 order_by=("visit",), 

355 collections="*", 

356 glob=("test_metric_comp",), 

357 ) 

358 

359 self.assertIn("increase this limit", cm.output[0]) 

360 

361 expectedTables = [ 

362 AstropyTable( 

363 array((("test_metric_comp", "ingest/run", "DummyCamComp", "423", "R", "d-r"),)), 

364 names=("type", "run", "instrument", "visit", "band", "physical_filter"), 

365 ), 

366 ] 

367 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

368 

369 # Same as previous test, but with positive limit so no warning is 

370 # issued. 

371 with self.assertNoLogs("lsst.daf.butler.script.queryDatasets", level="WARNING"): 

372 tables = self._queryDatasets( 

373 repo=testRepo.butler, 

374 limit=1, 

375 order_by=("visit",), 

376 collections="*", 

377 glob=("test_metric_comp",), 

378 ) 

379 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

380 

381 with self.assertLogs("lsst.daf.butler.script.queryDatasets", level="WARNING") as cm: 

382 tables = self._queryDatasets( 

383 repo=testRepo.butler, 

384 limit=-1, 

385 order_by=("-visit",), 

386 collections="*", 

387 glob=("test_metric_comp",), 

388 ) 

389 self.assertIn("increase this limit", cm.output[0]) 

390 

391 expectedTables = [ 

392 AstropyTable( 

393 array((("test_metric_comp", "ingest/run", "DummyCamComp", "424", "R", "d-r"),)), 

394 names=("type", "run", "instrument", "visit", "band", "physical_filter"), 

395 ), 

396 ] 

397 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

398 

399 # --order-by is not supported by the query backend for multiple dataset 

400 # types, so we can only provide it for queries with a single dataset 

401 # type. 

402 with self.assertRaisesRegex(NotImplementedError, "--order-by"): 

403 self._queryDatasets( 

404 repo=testRepo.butler, limit=1, order_by=("visit",), collections="*", glob=["*"] 

405 ) 

406 with self.assertRaisesRegex(NotImplementedError, "--order-by"): 

407 self._queryDatasets( 

408 repo=testRepo.butler, 

409 limit=1, 

410 order_by=("visit",), 

411 collections="*", 

412 glob=["test_metric_comp", "raw"], 

413 ) 

414 

415 def testFindFirstAndCollections(self): 

416 """Test the find-first option, and the collections option, since it 

417 is required for find-first. 

418 """ 

419 testRepo = MetricTestRepo(self.repoDir, configFile=self.configFile) 

420 

421 # Add a new run, and add a dataset to shadow an existing dataset. 

422 testRepo.addDataset(run="foo", dataId={"instrument": "DummyCamComp", "visit": 424}) 

423 

424 # Add a CHAINED collection to include the two runs, to check that 

425 # flattening works as well. 

426 testRepo.butler.collections.register("chain", CollectionType.CHAINED) 

427 testRepo.butler.collections.redefine_chain("chain", ["foo", "ingest/run"]) 

428 

429 # Verify that without find-first, duplicate datasets are returned 

430 tables = self._queryDatasets(repo=testRepo.butler, collections=["chain"], show_uri=True, glob="*") 

431 

432 # The test should be running with a single FileDatastore. 

433 roots = testRepo.butler.get_datastore_roots() 

434 assert len(roots) == 1 

435 datastore_root = list(roots.values())[0] 

436 

437 expectedTables = ( 

438 AstropyTable( 

439 array( 

440 ( 

441 ( 

442 "test_metric_comp.data", 

443 "foo", 

444 "DummyCamComp", 

445 "424", 

446 "R", 

447 "d-r", 

448 datastore_root.join( 

449 "foo/test_metric_comp.data/test_metric_comp_v00000424_fDummyCamComp_data.yaml" 

450 ), 

451 ), 

452 ( 

453 "test_metric_comp.data", 

454 "ingest/run", 

455 "DummyCamComp", 

456 "423", 

457 "R", 

458 "d-r", 

459 datastore_root.join( 

460 "ingest/run/test_metric_comp.data/" 

461 "test_metric_comp_v00000423_fDummyCamComp_data.yaml" 

462 ), 

463 ), 

464 ( 

465 "test_metric_comp.data", 

466 "ingest/run", 

467 "DummyCamComp", 

468 "424", 

469 "R", 

470 "d-r", 

471 datastore_root.join( 

472 "ingest/run/test_metric_comp.data/" 

473 "test_metric_comp_v00000424_fDummyCamComp_data.yaml" 

474 ), 

475 ), 

476 ) 

477 ), 

478 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

479 ), 

480 AstropyTable( 

481 array( 

482 ( 

483 ( 

484 "test_metric_comp.output", 

485 "foo", 

486 "DummyCamComp", 

487 "424", 

488 "R", 

489 "d-r", 

490 datastore_root.join( 

491 "foo/test_metric_comp.output/" 

492 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

493 ), 

494 ), 

495 ( 

496 "test_metric_comp.output", 

497 "ingest/run", 

498 "DummyCamComp", 

499 "423", 

500 "R", 

501 "d-r", 

502 datastore_root.join( 

503 "ingest/run/test_metric_comp.output/" 

504 "test_metric_comp_v00000423_fDummyCamComp_output.yaml" 

505 ), 

506 ), 

507 ( 

508 "test_metric_comp.output", 

509 "ingest/run", 

510 "DummyCamComp", 

511 "424", 

512 "R", 

513 "d-r", 

514 datastore_root.join( 

515 "ingest/run/test_metric_comp.output/" 

516 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

517 ), 

518 ), 

519 ) 

520 ), 

521 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

522 ), 

523 AstropyTable( 

524 array( 

525 ( 

526 ( 

527 "test_metric_comp.summary", 

528 "foo", 

529 "DummyCamComp", 

530 "424", 

531 "R", 

532 "d-r", 

533 datastore_root.join( 

534 "foo/test_metric_comp.summary/" 

535 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

536 ), 

537 ), 

538 ( 

539 "test_metric_comp.summary", 

540 "ingest/run", 

541 "DummyCamComp", 

542 "423", 

543 "R", 

544 "d-r", 

545 datastore_root.join( 

546 "ingest/run/test_metric_comp.summary/" 

547 "test_metric_comp_v00000423_fDummyCamComp_summary.yaml" 

548 ), 

549 ), 

550 ( 

551 "test_metric_comp.summary", 

552 "ingest/run", 

553 "DummyCamComp", 

554 "424", 

555 "R", 

556 "d-r", 

557 datastore_root.join( 

558 "ingest/run/test_metric_comp.summary/" 

559 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

560 ), 

561 ), 

562 ) 

563 ), 

564 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

565 ), 

566 ) 

567 

568 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

569 

570 # Verify that with find first the duplicate dataset is eliminated and 

571 # the more recent dataset is returned. 

572 tables = self._queryDatasets( 

573 repo=testRepo.butler, collections=["chain"], show_uri=True, find_first=True, glob="*" 

574 ) 

575 

576 expectedTables = ( 

577 AstropyTable( 

578 array( 

579 ( 

580 ( 

581 "test_metric_comp.data", 

582 "foo", 

583 "DummyCamComp", 

584 "424", 

585 "R", 

586 "d-r", 

587 datastore_root.join( 

588 "foo/test_metric_comp.data/test_metric_comp_v00000424_fDummyCamComp_data.yaml" 

589 ), 

590 ), 

591 ( 

592 "test_metric_comp.data", 

593 "ingest/run", 

594 "DummyCamComp", 

595 "423", 

596 "R", 

597 "d-r", 

598 datastore_root.join( 

599 "ingest/run/test_metric_comp.data/" 

600 "test_metric_comp_v00000423_fDummyCamComp_data.yaml" 

601 ), 

602 ), 

603 ) 

604 ), 

605 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

606 ), 

607 AstropyTable( 

608 array( 

609 ( 

610 ( 

611 "test_metric_comp.output", 

612 "foo", 

613 "DummyCamComp", 

614 "424", 

615 "R", 

616 "d-r", 

617 datastore_root.join( 

618 "foo/test_metric_comp.output/" 

619 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

620 ), 

621 ), 

622 ( 

623 "test_metric_comp.output", 

624 "ingest/run", 

625 "DummyCamComp", 

626 "423", 

627 "R", 

628 "d-r", 

629 datastore_root.join( 

630 "ingest/run/test_metric_comp.output/" 

631 "test_metric_comp_v00000423_fDummyCamComp_output.yaml" 

632 ), 

633 ), 

634 ) 

635 ), 

636 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

637 ), 

638 AstropyTable( 

639 array( 

640 ( 

641 ( 

642 "test_metric_comp.summary", 

643 "foo", 

644 "DummyCamComp", 

645 "424", 

646 "R", 

647 "d-r", 

648 datastore_root.join( 

649 "foo/test_metric_comp.summary/" 

650 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

651 ), 

652 ), 

653 ( 

654 "test_metric_comp.summary", 

655 "ingest/run", 

656 "DummyCamComp", 

657 "423", 

658 "R", 

659 "d-r", 

660 datastore_root.join( 

661 "ingest/run/test_metric_comp.summary/" 

662 "test_metric_comp_v00000423_fDummyCamComp_summary.yaml" 

663 ), 

664 ), 

665 ) 

666 ), 

667 names=("type", "run", "instrument", "visit", "band", "physical_filter", "URI"), 

668 ), 

669 ) 

670 

671 self.assertAstropyTablesEqual(tables, expectedTables, filterColumns=True) 

672 

673 # Verify that globs are not supported with find_first=True. 

674 with self.assertRaises(InvalidQueryError): 

675 self._queryDatasets( 

676 repo=testRepo.butler, collections=["*"], show_uri=True, find_first=True, glob="*" 

677 ) 

678 

679 # Collections argument will become mandatory soon 

680 with self.assertWarns(FutureWarning): 

681 self._queryDatasets(repo=testRepo.butler, glob="*") 

682 

683 

684if __name__ == "__main__": 

685 unittest.main()