lsst-felis 28.2024.4500__py3-none-any.whl → 29.2025.4500__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
felis/tap_schema.py CHANGED
@@ -21,13 +21,15 @@
21
21
  # You should have received a copy of the GNU General Public License
22
22
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
23
23
 
24
+ import csv
25
+ import io
24
26
  import logging
25
27
  import os
26
28
  import re
27
29
  from typing import Any
28
30
 
29
31
  from lsst.resources import ResourcePath
30
- from sqlalchemy import MetaData, Table, text
32
+ from sqlalchemy import MetaData, Table, select, text
31
33
  from sqlalchemy.engine import Connection, Engine
32
34
  from sqlalchemy.engine.mock import MockConnection
33
35
  from sqlalchemy.exc import SQLAlchemyError
@@ -35,13 +37,13 @@ from sqlalchemy.schema import CreateSchema
35
37
  from sqlalchemy.sql.dml import Insert
36
38
 
37
39
  from felis import datamodel
38
- from felis.datamodel import Schema
40
+ from felis.datamodel import Constraint, Schema
39
41
  from felis.db.utils import is_valid_engine
40
42
  from felis.metadata import MetaDataBuilder
41
43
 
42
44
  from .types import FelisType
43
45
 
44
- __all__ = ["TableManager", "DataLoader"]
46
+ __all__ = ["DataLoader", "TableManager"]
45
47
 
46
48
  logger = logging.getLogger(__name__)
47
49
 
@@ -91,9 +93,15 @@ class TableManager:
91
93
  self.table_name_postfix = table_name_postfix
92
94
  self.apply_schema_to_metadata = apply_schema_to_metadata
93
95
  self.schema_name = schema_name or TableManager._SCHEMA_NAME_STD
96
+ self.table_name_postfix = table_name_postfix
94
97
 
95
98
  if is_valid_engine(engine):
96
99
  assert isinstance(engine, Engine)
100
+ if table_name_postfix != "":
101
+ logger.warning(
102
+ "Table name postfix '%s' will be ignored when reflecting TAP_SCHEMA database",
103
+ table_name_postfix,
104
+ )
97
105
  logger.debug(
98
106
  "Reflecting TAP_SCHEMA database from existing database at %s",
99
107
  engine.url._replace(password="***"),
@@ -133,7 +141,7 @@ class TableManager:
133
141
  self._metadata = MetaDataBuilder(
134
142
  self.schema,
135
143
  apply_schema_to_metadata=self.apply_schema_to_metadata,
136
- apply_schema_to_tables=self.apply_schema_to_metadata,
144
+ table_name_postfix=self.table_name_postfix,
137
145
  ).build()
138
146
 
139
147
  logger.debug("Loaded TAP_SCHEMA '%s' from YAML resource", self.schema_name)
@@ -157,7 +165,7 @@ class TableManager:
157
165
  tables to be accessed by their standard TAP_SCHEMA names.
158
166
  """
159
167
  if table_name not in self._table_map:
160
- raise KeyError(f"Table '{table_name}' not found in table map")
168
+ raise KeyError(f"Table '{table_name}' not found in TAP_SCHEMA")
161
169
  return self.metadata.tables[self._table_map[table_name]]
162
170
 
163
171
  @property
@@ -202,7 +210,7 @@ class TableManager:
202
210
  str
203
211
  The path to the standard TAP_SCHEMA schema resource.
204
212
  """
205
- return os.path.join(os.path.dirname(__file__), "schemas", "tap_schema_std.yaml")
213
+ return os.path.join(os.path.dirname(__file__), "config", "tap_schema", "tap_schema_std.yaml")
206
214
 
207
215
  @classmethod
208
216
  def get_tap_schema_std_resource(cls) -> ResourcePath:
@@ -213,7 +221,7 @@ class TableManager:
213
221
  `~lsst.resources.ResourcePath`
214
222
  The standard TAP_SCHEMA schema resource.
215
223
  """
216
- return ResourcePath("resource://felis/schemas/tap_schema_std.yaml")
224
+ return ResourcePath("resource://felis/config/tap_schema/tap_schema_std.yaml")
217
225
 
218
226
  @classmethod
219
227
  def get_table_names_std(cls) -> list[str]:
@@ -355,10 +363,38 @@ class TableManager:
355
363
  engine
356
364
  The SQLAlchemy engine to use to create the tables.
357
365
  """
358
- logger.info("Creating TAP_SCHEMA database '%s'", self.metadata.schema)
366
+ logger.info("Creating TAP_SCHEMA database '%s'", self.schema_name)
359
367
  self._create_schema(engine)
360
368
  self.metadata.create_all(engine)
361
369
 
370
+ def select(self, engine: Engine, table_name: str, filter_condition: str = "") -> list[dict[str, Any]]:
371
+ """Select all rows from a TAP_SCHEMA table with an optional filter
372
+ condition.
373
+
374
+ Parameters
375
+ ----------
376
+ engine
377
+ The SQLAlchemy engine to use to connect to the database.
378
+ table_name
379
+ The name of the table to select from.
380
+ filter_condition
381
+ The filter condition as a string. If empty, no filter will be
382
+ applied.
383
+
384
+ Returns
385
+ -------
386
+ list
387
+ A list of dictionaries containing the rows from the table.
388
+ """
389
+ table = self[table_name]
390
+ query = select(table)
391
+ if filter_condition:
392
+ query = query.where(text(filter_condition))
393
+ with engine.connect() as connection:
394
+ result = connection.execute(query)
395
+ rows = [dict(row._mapping) for row in result]
396
+ return rows
397
+
362
398
 
363
399
  class DataLoader:
364
400
  """Load data into the TAP_SCHEMA tables.
@@ -380,6 +416,9 @@ class DataLoader:
380
416
  If True, print the SQL statements that will be executed.
381
417
  dry_run
382
418
  If True, the data will not be loaded into the database.
419
+ unique_keys
420
+ If True, prepend the schema name to the key name to make it unique
421
+ when loading data into the keys and key_columns tables.
383
422
  """
384
423
 
385
424
  def __init__(
@@ -391,6 +430,7 @@ class DataLoader:
391
430
  output_path: str | None = None,
392
431
  print_sql: bool = False,
393
432
  dry_run: bool = False,
433
+ unique_keys: bool = False,
394
434
  ):
395
435
  self.schema = schema
396
436
  self.mgr = mgr
@@ -400,6 +440,7 @@ class DataLoader:
400
440
  self.output_path = output_path
401
441
  self.print_sql = print_sql
402
442
  self.dry_run = dry_run
443
+ self.unique_keys = unique_keys
403
444
 
404
445
  def load(self) -> None:
405
446
  """Load the schema data into the TAP_SCHEMA tables.
@@ -426,10 +467,10 @@ class DataLoader:
426
467
  # Execute the inserts if not in dry run mode.
427
468
  self._execute_inserts()
428
469
  else:
429
- logger.info("Dry run: not loading data into database")
470
+ logger.info("Dry run - not loading data into database")
430
471
 
431
472
  def _insert_schemas(self) -> None:
432
- """Insert the schema data into the schemas table."""
473
+ """Insert the schema data into the ``schemas`` table."""
433
474
  schema_record = {
434
475
  "schema_name": self.schema.name,
435
476
  "utype": self.schema.votable_utype,
@@ -454,7 +495,7 @@ class DataLoader:
454
495
  return f"{self.schema.name}.{table.name}"
455
496
 
456
497
  def _insert_tables(self) -> None:
457
- """Insert the table data into the tables table."""
498
+ """Insert the table data into the ``tables`` table."""
458
499
  for table in self.schema.tables:
459
500
  table_record = {
460
501
  "schema_name": self.schema.name,
@@ -467,7 +508,7 @@ class DataLoader:
467
508
  self._insert("tables", table_record)
468
509
 
469
510
  def _insert_columns(self) -> None:
470
- """Insert the column data into the columns table."""
511
+ """Insert the column data into the ``columns`` table."""
471
512
  for table in self.schema.tables:
472
513
  for column in table.columns:
473
514
  felis_type = FelisType.felis_type(column.datatype.value)
@@ -495,18 +536,49 @@ class DataLoader:
495
536
  }
496
537
  self._insert("columns", column_record)
497
538
 
539
+ def _get_key(self, constraint: Constraint) -> str:
540
+ """Get the key name for a constraint.
541
+
542
+ Parameters
543
+ ----------
544
+ constraint
545
+ The constraint to get the key name for.
546
+
547
+ Returns
548
+ -------
549
+ str
550
+ The key name for the constraint.
551
+
552
+ Notes
553
+ -----
554
+ This will prepend the name of the schema to the key name if the
555
+ `unique_keys` attribute is set to True. Otherwise, it will just return
556
+ the name of the constraint.
557
+ """
558
+ if self.unique_keys:
559
+ key_id = f"{self.schema.name}_{constraint.name}"
560
+ logger.debug("Generated unique key_id: %s -> %s", constraint.name, key_id)
561
+ else:
562
+ key_id = constraint.name
563
+ return key_id
564
+
498
565
  def _insert_keys(self) -> None:
499
- """Insert the foreign keys into the keys and key_columns tables."""
566
+ """Insert the foreign keys into the ``keys`` and ``key_columns``
567
+ tables.
568
+ """
500
569
  for table in self.schema.tables:
501
570
  for constraint in table.constraints:
502
571
  if isinstance(constraint, datamodel.ForeignKeyConstraint):
572
+ ###########################################################
503
573
  # Handle keys table
574
+ ###########################################################
504
575
  referenced_column = self.schema.find_object_by_id(
505
576
  constraint.referenced_columns[0], datamodel.Column
506
577
  )
507
578
  referenced_table = self.schema.get_table_by_column(referenced_column)
579
+ key_id = self._get_key(constraint)
508
580
  key_record = {
509
- "key_id": constraint.name,
581
+ "key_id": key_id,
510
582
  "from_table": self._get_table_name(table),
511
583
  "target_table": self._get_table_name(referenced_table),
512
584
  "description": constraint.description,
@@ -514,17 +586,23 @@ class DataLoader:
514
586
  }
515
587
  self._insert("keys", key_record)
516
588
 
589
+ ###########################################################
517
590
  # Handle key_columns table
518
- from_column = self.schema.find_object_by_id(constraint.columns[0], datamodel.Column)
519
- target_column = self.schema.find_object_by_id(
520
- constraint.referenced_columns[0], datamodel.Column
521
- )
522
- key_columns_record = {
523
- "key_id": constraint.name,
524
- "from_column": from_column.name,
525
- "target_column": target_column.name,
526
- }
527
- self._insert("key_columns", key_columns_record)
591
+ ###########################################################
592
+ # Loop over the corresponding columns and referenced
593
+ # columns and insert a record for each pair. This is
594
+ # necessary for proper handling of composite keys.
595
+ for from_column_id, target_column_id in zip(
596
+ constraint.columns, constraint.referenced_columns
597
+ ):
598
+ from_column = self.schema.find_object_by_id(from_column_id, datamodel.Column)
599
+ target_column = self.schema.find_object_by_id(target_column_id, datamodel.Column)
600
+ key_columns_record = {
601
+ "key_id": key_id,
602
+ "from_column": from_column.name,
603
+ "target_column": target_column.name,
604
+ }
605
+ self._insert("key_columns", key_columns_record)
528
606
 
529
607
  def _generate_all_inserts(self) -> None:
530
608
  """Generate the inserts for all the data."""
@@ -567,7 +645,7 @@ class DataLoader:
567
645
  def _print_sql(self) -> None:
568
646
  """Print the generated inserts to stdout."""
569
647
  for insert_str in self._compiled_inserts():
570
- print(insert_str)
648
+ print(insert_str + ";")
571
649
 
572
650
  def _write_sql_to_file(self) -> None:
573
651
  """Write the generated insert statements to a file."""
@@ -575,7 +653,7 @@ class DataLoader:
575
653
  raise ValueError("No output path specified")
576
654
  with open(self.output_path, "w") as outfile:
577
655
  for insert_str in self._compiled_inserts():
578
- outfile.write(insert_str + "\n")
656
+ outfile.write(insert_str + ";" + "\n")
579
657
 
580
658
  def _insert(self, table_name: str, record: list[Any] | dict[str, Any]) -> None:
581
659
  """Generate an insert statement for a record.
@@ -642,3 +720,48 @@ class DataLoader:
642
720
  if index.columns and len(index.columns) == 1 and index.columns[0] == column.id:
643
721
  return 1
644
722
  return 0
723
+
724
+
725
+ class MetadataInserter:
726
+ """Insert TAP_SCHEMA self-description rows into the database.
727
+
728
+ Parameters
729
+ ----------
730
+ mgr
731
+ The table manager that contains the TAP_SCHEMA tables.
732
+ engine
733
+ The engine for connecting to the TAP_SCHEMA database.
734
+ """
735
+
736
+ def __init__(self, mgr: TableManager, engine: Engine):
737
+ """Initialize the metadata inserter.
738
+
739
+ Parameters
740
+ ----------
741
+ mgr
742
+ The table manager representing the TAP_SCHEMA tables.
743
+ engine
744
+ The SQLAlchemy engine for connecting to the database.
745
+ """
746
+ self._mgr = mgr
747
+ self._engine = engine
748
+
749
+ def insert_metadata(self) -> None:
750
+ """Insert the TAP_SCHEMA metadata into the database."""
751
+ for table_name in self._mgr.get_table_names_std():
752
+ table = self._mgr[table_name]
753
+ csv_bytes = ResourcePath(f"resource://felis/config/tap_schema/{table_name}.csv").read()
754
+ text_stream = io.TextIOWrapper(io.BytesIO(csv_bytes), encoding="utf-8")
755
+ reader = csv.reader(text_stream)
756
+ headers = next(reader)
757
+ rows = [
758
+ {key: None if value == "\\N" else value for key, value in zip(headers, row)} for row in reader
759
+ ]
760
+ logger.debug(
761
+ "Inserting %d rows into table '%s' with headers: %s",
762
+ len(rows),
763
+ table_name,
764
+ headers,
765
+ )
766
+ with self._engine.begin() as conn:
767
+ conn.execute(table.insert(), rows)
felis/tests/run_cli.py ADDED
@@ -0,0 +1,79 @@
1
+ """Test utility for running cli commands."""
2
+
3
+ # This file is part of felis.
4
+ #
5
+ # Developed for the LSST Data Management System.
6
+ # This product includes software developed by the LSST Project
7
+ # (https://www.lsst.org).
8
+ # See the COPYRIGHT file at the top-level directory of this distribution
9
+ # for details of code ownership.
10
+ #
11
+ # This program is free software: you can redistribute it and/or modify
12
+ # it under the terms of the GNU General Public License as published by
13
+ # the Free Software Foundation, either version 3 of the License, or
14
+ # (at your option) any later version.
15
+ #
16
+ # This program is distributed in the hope that it will be useful,
17
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ # GNU General Public License for more details.
20
+ #
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
23
+
24
+ import logging
25
+
26
+ from click.testing import CliRunner
27
+
28
+ from felis.cli import cli
29
+
30
+ __all__ = ["run_cli"]
31
+
32
+
33
+ def run_cli(
34
+ cmd: list[str],
35
+ log_level: int = logging.WARNING,
36
+ catch_exceptions: bool = False,
37
+ expect_error: bool = False,
38
+ print_cmd: bool = False,
39
+ print_output: bool = False,
40
+ id_generation: bool = False,
41
+ ) -> None:
42
+ """Run a CLI command and check the exit code.
43
+
44
+ Parameters
45
+ ----------
46
+ cmd : list[str]
47
+ The command to run.
48
+ log_level : int
49
+ The logging level to use, by default logging.WARNING.
50
+ catch_exceptions : bool
51
+ Whether to catch exceptions, by default False.
52
+ expect_error : bool
53
+ Whether to expect an error, by default False.
54
+ print_cmd : bool
55
+ Whether to print the command, by default False.
56
+ print_output : bool
57
+ Whether to print the output, by default False.
58
+ id_generation : bool
59
+ Whether to enable id generation, by default False.
60
+ """
61
+ if not cmd:
62
+ raise ValueError("No command provided.")
63
+ full_cmd = ["--log-level", logging.getLevelName(log_level)] + cmd
64
+ if id_generation:
65
+ full_cmd = ["--id-generation"] + full_cmd
66
+ if print_cmd:
67
+ print(f"Running command: felis {' '.join(full_cmd)}")
68
+ runner = CliRunner()
69
+ result = runner.invoke(
70
+ cli,
71
+ full_cmd,
72
+ catch_exceptions=catch_exceptions,
73
+ )
74
+ if print_output:
75
+ print(result.output)
76
+ if expect_error:
77
+ assert result.exit_code != 0
78
+ else:
79
+ assert result.exit_code == 0
felis/types.py CHANGED
@@ -26,20 +26,20 @@ from __future__ import annotations
26
26
  from typing import Any
27
27
 
28
28
  __all__ = [
29
- "FelisType",
29
+ "Binary",
30
30
  "Boolean",
31
31
  "Byte",
32
- "Short",
32
+ "Char",
33
+ "Double",
34
+ "FelisType",
35
+ "Float",
33
36
  "Int",
34
37
  "Long",
35
- "Float",
36
- "Double",
37
- "Char",
38
+ "Short",
38
39
  "String",
39
- "Unicode",
40
40
  "Text",
41
- "Binary",
42
41
  "Timestamp",
42
+ "Unicode",
43
43
  ]
44
44
 
45
45
 
@@ -1,34 +1,38 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lsst-felis
3
- Version: 28.2024.4500
3
+ Version: 29.2025.4500
4
4
  Summary: A vocabulary for describing catalogs and acting on those descriptions
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
- License: GNU General Public License v3 or later (GPLv3+)
6
+ License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://felis.lsst.io
8
8
  Project-URL: Source, https://github.com/lsst/felis
9
9
  Keywords: lsst
10
10
  Classifier: Intended Audience :: Science/Research
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
11
  Classifier: Operating System :: OS Independent
13
12
  Classifier: Programming Language :: Python :: 3
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
16
17
  Classifier: Topic :: Scientific/Engineering :: Astronomy
17
18
  Requires-Python: >=3.11.0
18
19
  Description-Content-Type: text/markdown
19
20
  License-File: COPYRIGHT
20
21
  License-File: LICENSE
21
- Requires-Dist: astropy >=4
22
- Requires-Dist: sqlalchemy >=1.4
23
- Requires-Dist: click >=7
24
- Requires-Dist: pyyaml >=6
25
- Requires-Dist: pydantic <3,>=2
26
- Requires-Dist: lsst-utils
22
+ Requires-Dist: alembic
23
+ Requires-Dist: astropy
24
+ Requires-Dist: click
25
+ Requires-Dist: deepdiff
27
26
  Requires-Dist: lsst-resources
28
- Provides-Extra: dev
29
- Requires-Dist: documenteer[guide] <2 ; extra == 'dev'
30
- Requires-Dist: autodoc-pydantic ; extra == 'dev'
31
- Requires-Dist: sphinx-click ; extra == 'dev'
27
+ Requires-Dist: lsst-utils
28
+ Requires-Dist: numpy
29
+ Requires-Dist: pydantic<3,>=2
30
+ Requires-Dist: pyyaml
31
+ Requires-Dist: sqlalchemy
32
32
  Provides-Extra: test
33
- Requires-Dist: pytest >=3.2 ; extra == 'test'
34
-
33
+ Requires-Dist: pytest>=3.2; extra == "test"
34
+ Provides-Extra: dev
35
+ Requires-Dist: documenteer[guide]<2; extra == "dev"
36
+ Requires-Dist: autodoc_pydantic; extra == "dev"
37
+ Requires-Dist: sphinx-click; extra == "dev"
38
+ Dynamic: license-file
@@ -0,0 +1,31 @@
1
+ felis/__init__.py,sha256=HnwWzLaPOSnPzAoppSIHzTrGfixEgvkzJdBxa8-03cw,1294
2
+ felis/cli.py,sha256=g6OrBrIylNLiflSvrLlef86BjoiehV3L5eAvVPrxPog,16911
3
+ felis/datamodel.py,sha256=VAJ9DqOBtfu3fWtpDOcpg4Ca1jfq8NMG6MHH8GbHpl0,52135
4
+ felis/diff.py,sha256=ZzjOJ57p5ZwFn6eem7CYoPjSnxti5OZY33B6Ds5Q-Rg,7797
5
+ felis/metadata.py,sha256=79YcaIqeFP-pj9zhWpqXlvw_piUTUwuLrV5_8eVYalQ,13763
6
+ felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ felis/tap_schema.py,sha256=Kfpnv6AO_ni_B-DEgi3PRSkGKkTEybuYfWlA9gOMM_I,27071
8
+ felis/types.py,sha256=ifZQjc-Uw5CM3L7hmFUb7wcHY1O_HgJCw6HPqyUkHvk,5510
9
+ felis/config/tap_schema/columns.csv,sha256=9RsyuPObUQ_6myux9vKtlQ-aJgs7rvvxoLf6yYSRWqc,3272
10
+ felis/config/tap_schema/key_columns.csv,sha256=dRezco5ltcM1mG--2DvPsbOxB6cwVaBwczwi3It2vag,210
11
+ felis/config/tap_schema/keys.csv,sha256=6zTXyo-1GNfu5sBWpX-7ZJFAtHrxOys78AViCcdPgu8,377
12
+ felis/config/tap_schema/schemas.csv,sha256=z5g1bW1Y9H8nKLZyH4e5xiBBoK9JezR2Xf8L79K2TZk,138
13
+ felis/config/tap_schema/tables.csv,sha256=o0KioOiL7hw9ntCyKWili-iFMjAaGRMUOE-nM30LBD0,510
14
+ felis/config/tap_schema/tap_schema_std.yaml,sha256=sPW-Vk72nY0PFpCvP5d8L8fWvhkif-x32sGtcfDZ8bU,7131
15
+ felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ felis/db/dialects.py,sha256=XTZEbTnfy96GJDNRLCQMbAV6irerC87vhO_HyTIXLbs,3517
17
+ felis/db/schema.py,sha256=NOFXzBoBQcgpoRlgT3LoC70FKp7pCSmFEJ7rU8FIT-c,2101
18
+ felis/db/sqltypes.py,sha256=Q2p3Af3O5-B1ZxQ4M2j_w8SH1o_kp6ezg8h7LmSlfww,11060
19
+ felis/db/utils.py,sha256=jiKQ_SirKRdQITHe8gSiT_i3ckRHZbkAnwUlEHk2u4Y,14116
20
+ felis/db/variants.py,sha256=eahthrbVeV8ZdGamWQccNmWgx6CCscGrU0vQRs5HZK8,5260
21
+ felis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ felis/tests/postgresql.py,sha256=B_xk4fLual5-viGDqP20r94okuc0pbSvytRH_L0fvMs,4035
23
+ felis/tests/run_cli.py,sha256=Gg8loUIGj9t6KlkRKrEc9Z9b5dtlkpJy94ORuj4BrxU,2503
24
+ lsst_felis-29.2025.4500.dist-info/licenses/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
25
+ lsst_felis-29.2025.4500.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
26
+ lsst_felis-29.2025.4500.dist-info/METADATA,sha256=_brJSKPh9-Izo-XR_3FTFQb_zfsnECQyL2t6gjLW6p8,1377
27
+ lsst_felis-29.2025.4500.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ lsst_felis-29.2025.4500.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
29
+ lsst_felis-29.2025.4500.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
30
+ lsst_felis-29.2025.4500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
+ lsst_felis-29.2025.4500.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5