Coverage for tests/test_cliCmdQueryDatasets.py: 22%

111 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-21 01:20 -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 self.enterContext(testRepo.butler) 

177 

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

179 

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

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

182 # datastore. 

183 roots = testRepo.butler.get_datastore_roots() 

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

185 

186 self.assertAstropyTablesEqual( 

187 tables, 

188 expectedFilesystemDatastoreTables(datastore_root), 

189 filterColumns=True, 

190 ) 

191 

192 def testShowURI(self): 

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

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

195 self.enterContext(testRepo.butler) 

196 

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

198 

199 roots = testRepo.butler.get_datastore_roots() 

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

201 

202 self.assertAstropyTablesEqual( 

203 tables, expectedFilesystemDatastoreTables(datastore_root), filterColumns=True 

204 ) 

205 

206 def testShowUriNoDisassembly(self): 

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

208 testRepo = MetricTestRepo( 

209 self.repoDir, 

210 configFile=self.configFile, 

211 storageClassName="StructuredCompositeReadCompNoDisassembly", 

212 ) 

213 self.enterContext(testRepo.butler) 

214 

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

216 

217 roots = testRepo.butler.get_datastore_roots() 

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

219 

220 expected = [ 

221 AstropyTable( 

222 array( 

223 ( 

224 ( 

225 "test_metric_comp", 

226 "ingest/run", 

227 "DummyCamComp", 

228 "423", 

229 "R", 

230 "d-r", 

231 datastore_root.join( 

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

233 ), 

234 ), 

235 ( 

236 "test_metric_comp", 

237 "ingest/run", 

238 "DummyCamComp", 

239 "424", 

240 "R", 

241 "d-r", 

242 datastore_root.join( 

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

244 ), 

245 ), 

246 ) 

247 ), 

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

249 ), 

250 ] 

251 

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

253 

254 def testNoShowURI(self): 

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

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

257 self.enterContext(testRepo.butler) 

258 

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

260 

261 expectedTables = ( 

262 AstropyTable( 

263 array( 

264 ( 

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

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

267 ) 

268 ), 

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

270 ), 

271 ) 

272 

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

274 

275 def testWhere(self): 

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

277 queryDatasets. 

278 """ 

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

280 self.enterContext(testRepo.butler) 

281 

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

283 with self.subTest(glob=glob): 

284 tables = self._queryDatasets( 

285 repo=testRepo.butler, 

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

287 collections="*", 

288 glob=glob, 

289 ) 

290 

291 expectedTables = ( 

292 AstropyTable( 

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

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

295 ), 

296 ) 

297 

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

299 

300 with self.assertRaises(InvalidQueryError): 

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

302 

303 def testGlobDatasetType(self): 

304 """Test specifying dataset type.""" 

305 # Create and register an additional DatasetType 

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

307 self.enterContext(testRepo.butler) 

308 

309 testRepo.butler.registry.insertDimensionData( 

310 "visit", 

311 { 

312 "instrument": "DummyCamComp", 

313 "id": 425, 

314 "name": "fourtwentyfive", 

315 "physical_filter": "d-r", 

316 "day_obs": 20200101, 

317 }, 

318 ) 

319 

320 datasetType = addDatasetType( 

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

322 ) 

323 

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

325 

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

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

328 

329 expectedTables = ( 

330 AstropyTable( 

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

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

333 ), 

334 AstropyTable( 

335 array( 

336 ( 

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

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

339 ) 

340 ), 

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

342 ), 

343 ) 

344 

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

346 

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

348 with self.assertWarns(FutureWarning): 

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

350 

351 def test_limit_order(self): 

352 """Test limit and ordering.""" 

353 # Create and register an additional DatasetType 

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

355 self.enterContext(testRepo.butler) 

356 

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

358 tables = self._queryDatasets( 

359 repo=testRepo.butler, 

360 limit=-1, 

361 order_by=("visit",), 

362 collections="*", 

363 glob=("test_metric_comp",), 

364 ) 

365 

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

367 

368 expectedTables = [ 

369 AstropyTable( 

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

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

372 ), 

373 ] 

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

375 

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

377 # issued. 

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

379 tables = self._queryDatasets( 

380 repo=testRepo.butler, 

381 limit=1, 

382 order_by=("visit",), 

383 collections="*", 

384 glob=("test_metric_comp",), 

385 ) 

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

387 

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

389 tables = self._queryDatasets( 

390 repo=testRepo.butler, 

391 limit=-1, 

392 order_by=("-visit",), 

393 collections="*", 

394 glob=("test_metric_comp",), 

395 ) 

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

397 

398 expectedTables = [ 

399 AstropyTable( 

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

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

402 ), 

403 ] 

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

405 

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

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

408 # type. 

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

410 self._queryDatasets( 

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

412 ) 

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

414 self._queryDatasets( 

415 repo=testRepo.butler, 

416 limit=1, 

417 order_by=("visit",), 

418 collections="*", 

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

420 ) 

421 

422 def testFindFirstAndCollections(self): 

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

424 is required for find-first. 

425 """ 

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

427 self.enterContext(testRepo.butler) 

428 

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

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

431 

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

433 # flattening works as well. 

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

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

436 

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

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

439 

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

441 roots = testRepo.butler.get_datastore_roots() 

442 assert len(roots) == 1 

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

444 

445 expectedTables = ( 

446 AstropyTable( 

447 array( 

448 ( 

449 ( 

450 "test_metric_comp.data", 

451 "foo", 

452 "DummyCamComp", 

453 "424", 

454 "R", 

455 "d-r", 

456 datastore_root.join( 

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

458 ), 

459 ), 

460 ( 

461 "test_metric_comp.data", 

462 "ingest/run", 

463 "DummyCamComp", 

464 "423", 

465 "R", 

466 "d-r", 

467 datastore_root.join( 

468 "ingest/run/test_metric_comp.data/" 

469 "test_metric_comp_v00000423_fDummyCamComp_data.yaml" 

470 ), 

471 ), 

472 ( 

473 "test_metric_comp.data", 

474 "ingest/run", 

475 "DummyCamComp", 

476 "424", 

477 "R", 

478 "d-r", 

479 datastore_root.join( 

480 "ingest/run/test_metric_comp.data/" 

481 "test_metric_comp_v00000424_fDummyCamComp_data.yaml" 

482 ), 

483 ), 

484 ) 

485 ), 

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

487 ), 

488 AstropyTable( 

489 array( 

490 ( 

491 ( 

492 "test_metric_comp.output", 

493 "foo", 

494 "DummyCamComp", 

495 "424", 

496 "R", 

497 "d-r", 

498 datastore_root.join( 

499 "foo/test_metric_comp.output/" 

500 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

501 ), 

502 ), 

503 ( 

504 "test_metric_comp.output", 

505 "ingest/run", 

506 "DummyCamComp", 

507 "423", 

508 "R", 

509 "d-r", 

510 datastore_root.join( 

511 "ingest/run/test_metric_comp.output/" 

512 "test_metric_comp_v00000423_fDummyCamComp_output.yaml" 

513 ), 

514 ), 

515 ( 

516 "test_metric_comp.output", 

517 "ingest/run", 

518 "DummyCamComp", 

519 "424", 

520 "R", 

521 "d-r", 

522 datastore_root.join( 

523 "ingest/run/test_metric_comp.output/" 

524 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

525 ), 

526 ), 

527 ) 

528 ), 

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

530 ), 

531 AstropyTable( 

532 array( 

533 ( 

534 ( 

535 "test_metric_comp.summary", 

536 "foo", 

537 "DummyCamComp", 

538 "424", 

539 "R", 

540 "d-r", 

541 datastore_root.join( 

542 "foo/test_metric_comp.summary/" 

543 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

544 ), 

545 ), 

546 ( 

547 "test_metric_comp.summary", 

548 "ingest/run", 

549 "DummyCamComp", 

550 "423", 

551 "R", 

552 "d-r", 

553 datastore_root.join( 

554 "ingest/run/test_metric_comp.summary/" 

555 "test_metric_comp_v00000423_fDummyCamComp_summary.yaml" 

556 ), 

557 ), 

558 ( 

559 "test_metric_comp.summary", 

560 "ingest/run", 

561 "DummyCamComp", 

562 "424", 

563 "R", 

564 "d-r", 

565 datastore_root.join( 

566 "ingest/run/test_metric_comp.summary/" 

567 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

568 ), 

569 ), 

570 ) 

571 ), 

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

573 ), 

574 ) 

575 

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

577 

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

579 # the more recent dataset is returned. 

580 tables = self._queryDatasets( 

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

582 ) 

583 

584 expectedTables = ( 

585 AstropyTable( 

586 array( 

587 ( 

588 ( 

589 "test_metric_comp.data", 

590 "foo", 

591 "DummyCamComp", 

592 "424", 

593 "R", 

594 "d-r", 

595 datastore_root.join( 

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

597 ), 

598 ), 

599 ( 

600 "test_metric_comp.data", 

601 "ingest/run", 

602 "DummyCamComp", 

603 "423", 

604 "R", 

605 "d-r", 

606 datastore_root.join( 

607 "ingest/run/test_metric_comp.data/" 

608 "test_metric_comp_v00000423_fDummyCamComp_data.yaml" 

609 ), 

610 ), 

611 ) 

612 ), 

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

614 ), 

615 AstropyTable( 

616 array( 

617 ( 

618 ( 

619 "test_metric_comp.output", 

620 "foo", 

621 "DummyCamComp", 

622 "424", 

623 "R", 

624 "d-r", 

625 datastore_root.join( 

626 "foo/test_metric_comp.output/" 

627 "test_metric_comp_v00000424_fDummyCamComp_output.yaml" 

628 ), 

629 ), 

630 ( 

631 "test_metric_comp.output", 

632 "ingest/run", 

633 "DummyCamComp", 

634 "423", 

635 "R", 

636 "d-r", 

637 datastore_root.join( 

638 "ingest/run/test_metric_comp.output/" 

639 "test_metric_comp_v00000423_fDummyCamComp_output.yaml" 

640 ), 

641 ), 

642 ) 

643 ), 

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

645 ), 

646 AstropyTable( 

647 array( 

648 ( 

649 ( 

650 "test_metric_comp.summary", 

651 "foo", 

652 "DummyCamComp", 

653 "424", 

654 "R", 

655 "d-r", 

656 datastore_root.join( 

657 "foo/test_metric_comp.summary/" 

658 "test_metric_comp_v00000424_fDummyCamComp_summary.yaml" 

659 ), 

660 ), 

661 ( 

662 "test_metric_comp.summary", 

663 "ingest/run", 

664 "DummyCamComp", 

665 "423", 

666 "R", 

667 "d-r", 

668 datastore_root.join( 

669 "ingest/run/test_metric_comp.summary/" 

670 "test_metric_comp_v00000423_fDummyCamComp_summary.yaml" 

671 ), 

672 ), 

673 ) 

674 ), 

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

676 ), 

677 ) 

678 

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

680 

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

682 with self.assertRaises(InvalidQueryError): 

683 self._queryDatasets( 

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

685 ) 

686 

687 # Collections argument will become mandatory soon 

688 with self.assertWarns(FutureWarning): 

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

690 

691 

692if __name__ == "__main__": 

693 unittest.main()