lsst-felis 28.2024.4800__py3-none-any.whl → 28.2025.500__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.
Potentially problematic release.
This version of lsst-felis might be problematic. Click here for more details.
- felis/__init__.py +4 -0
- felis/cli.py +95 -167
- felis/datamodel.py +131 -6
- felis/db/schema.py +62 -0
- felis/diff.py +229 -0
- felis/metadata.py +5 -8
- felis/tap_schema.py +11 -5
- felis/version.py +1 -1
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/METADATA +12 -8
- lsst_felis-28.2025.500.dist-info/RECORD +26 -0
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/WHEEL +1 -1
- felis/tap.py +0 -597
- felis/tests/utils.py +0 -122
- lsst_felis-28.2024.4800.dist-info/RECORD +0 -26
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/COPYRIGHT +0 -0
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/LICENSE +0 -0
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/entry_points.txt +0 -0
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/top_level.txt +0 -0
- {lsst_felis-28.2024.4800.dist-info → lsst_felis-28.2025.500.dist-info}/zip-safe +0 -0
felis/diff.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Compare schemas and print the differences."""
|
|
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
|
+
import pprint
|
|
26
|
+
import re
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from alembic.autogenerate import compare_metadata
|
|
31
|
+
from alembic.migration import MigrationContext
|
|
32
|
+
from deepdiff.diff import DeepDiff
|
|
33
|
+
from sqlalchemy import Engine, MetaData
|
|
34
|
+
|
|
35
|
+
from .datamodel import Schema
|
|
36
|
+
from .metadata import MetaDataBuilder
|
|
37
|
+
|
|
38
|
+
__all__ = ["SchemaDiff", "DatabaseDiff"]
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# Change alembic log level to avoid unnecessary output
|
|
43
|
+
logging.getLogger("alembic").setLevel(logging.WARNING)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SchemaDiff:
|
|
47
|
+
"""
|
|
48
|
+
Compare two schemas using DeepDiff and print the differences.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
schema1
|
|
53
|
+
The first schema to compare.
|
|
54
|
+
schema2
|
|
55
|
+
The second schema to compare.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, schema1: Schema, schema2: Schema):
|
|
59
|
+
self.dict1 = schema1.model_dump(exclude_none=True)
|
|
60
|
+
self.dict2 = schema2.model_dump(exclude_none=True)
|
|
61
|
+
self.diff = DeepDiff(self.dict1, self.dict2, ignore_order=True)
|
|
62
|
+
|
|
63
|
+
def print(self) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Print the differences between the two schemas.
|
|
66
|
+
"""
|
|
67
|
+
pprint.pprint(self.diff)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def has_changes(self) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Check if there are any differences between the two schemas.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
bool
|
|
77
|
+
True if there are differences, False otherwise.
|
|
78
|
+
"""
|
|
79
|
+
return len(self.diff) > 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class FormattedSchemaDiff(SchemaDiff):
|
|
83
|
+
"""
|
|
84
|
+
Compare two schemas using DeepDiff and print the differences using a
|
|
85
|
+
customized output format.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
schema1
|
|
90
|
+
The first schema to compare.
|
|
91
|
+
schema2
|
|
92
|
+
The second schema to compare.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, schema1: Schema, schema2: Schema):
|
|
96
|
+
super().__init__(schema1, schema2)
|
|
97
|
+
|
|
98
|
+
def print(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Print the differences between the two schemas using a custom format.
|
|
101
|
+
"""
|
|
102
|
+
handlers: dict[str, Callable[[dict[str, Any]], None]] = {
|
|
103
|
+
"values_changed": self._handle_values_changed,
|
|
104
|
+
"iterable_item_added": self._handle_iterable_item_added,
|
|
105
|
+
"iterable_item_removed": self._handle_iterable_item_removed,
|
|
106
|
+
"dictionary_item_added": self._handle_dictionary_item_added,
|
|
107
|
+
"dictionary_item_removed": self._handle_dictionary_item_removed,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for change_type, handler in handlers.items():
|
|
111
|
+
if change_type in self.diff:
|
|
112
|
+
handler(self.diff[change_type])
|
|
113
|
+
|
|
114
|
+
def _print_header(self, id_dict: dict[str, Any], keys: list[int | str]) -> None:
|
|
115
|
+
id = self._get_id(id_dict, keys)
|
|
116
|
+
print(f"{id} @ {self._get_key_display(keys)}")
|
|
117
|
+
|
|
118
|
+
def _handle_values_changed(self, changes: dict[str, Any]) -> None:
|
|
119
|
+
for key in changes:
|
|
120
|
+
keys = self._parse_deepdiff_path(key)
|
|
121
|
+
value1 = self._get_value_from_keys(self.dict1, keys)
|
|
122
|
+
value2 = self._get_value_from_keys(self.dict2, keys)
|
|
123
|
+
self._print_header(self.dict1, keys)
|
|
124
|
+
print(f"- {value1}")
|
|
125
|
+
print(f"+ {value2}")
|
|
126
|
+
|
|
127
|
+
def _handle_iterable_item_added(self, changes: dict[str, Any]) -> None:
|
|
128
|
+
for key in changes:
|
|
129
|
+
keys = self._parse_deepdiff_path(key)
|
|
130
|
+
value = self._get_value_from_keys(self.dict2, keys)
|
|
131
|
+
self._print_header(self.dict2, keys)
|
|
132
|
+
print(f"+ {value}")
|
|
133
|
+
|
|
134
|
+
def _handle_iterable_item_removed(self, changes: dict[str, Any]) -> None:
|
|
135
|
+
for key in changes:
|
|
136
|
+
keys = self._parse_deepdiff_path(key)
|
|
137
|
+
value = self._get_value_from_keys(self.dict1, keys)
|
|
138
|
+
self._print_header(self.dict1, keys)
|
|
139
|
+
print(f"- {value}")
|
|
140
|
+
|
|
141
|
+
def _handle_dictionary_item_added(self, changes: dict[str, Any]) -> None:
|
|
142
|
+
for key in changes:
|
|
143
|
+
keys = self._parse_deepdiff_path(key)
|
|
144
|
+
value = self._get_value_from_keys(self.dict2, keys)
|
|
145
|
+
self._print_header(self.dict2, keys)
|
|
146
|
+
print(f"+ {value}")
|
|
147
|
+
|
|
148
|
+
def _handle_dictionary_item_removed(self, changes: dict[str, Any]) -> None:
|
|
149
|
+
for key in changes:
|
|
150
|
+
keys = self._parse_deepdiff_path(key)
|
|
151
|
+
value = self._get_value_from_keys(self.dict1, keys)
|
|
152
|
+
self._print_header(self.dict1, keys)
|
|
153
|
+
print(f"- {value}")
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _get_id(values: dict, keys: list[str | int]) -> str:
|
|
157
|
+
value = values
|
|
158
|
+
last_id = None
|
|
159
|
+
|
|
160
|
+
for key in keys:
|
|
161
|
+
if isinstance(value, dict) and "id" in value:
|
|
162
|
+
last_id = value["id"]
|
|
163
|
+
value = value[key]
|
|
164
|
+
|
|
165
|
+
if isinstance(value, dict) and "id" in value:
|
|
166
|
+
last_id = value["id"]
|
|
167
|
+
|
|
168
|
+
if last_id is not None:
|
|
169
|
+
return last_id
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError("No 'id' found in the specified path")
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _get_key_display(keys: list[str | int]) -> str:
|
|
175
|
+
return ".".join(str(k) for k in keys)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _parse_deepdiff_path(path: str) -> list[str | int]:
|
|
179
|
+
if path.startswith("root"):
|
|
180
|
+
path = path[4:]
|
|
181
|
+
|
|
182
|
+
pattern = re.compile(r"\['([^']+)'\]|\[(\d+)\]")
|
|
183
|
+
matches = pattern.findall(path)
|
|
184
|
+
|
|
185
|
+
keys = []
|
|
186
|
+
for match in matches:
|
|
187
|
+
if match[0]: # String key
|
|
188
|
+
keys.append(match[0])
|
|
189
|
+
elif match[1]: # Integer index
|
|
190
|
+
keys.append(int(match[1]))
|
|
191
|
+
|
|
192
|
+
return keys
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _get_value_from_keys(data: dict, keys: list[str | int]) -> Any:
|
|
196
|
+
value = data
|
|
197
|
+
for key in keys:
|
|
198
|
+
value = value[key]
|
|
199
|
+
return value
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class DatabaseDiff(SchemaDiff):
|
|
203
|
+
"""
|
|
204
|
+
Compare a schema with a database and print the differences.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
schema
|
|
209
|
+
The schema to compare.
|
|
210
|
+
engine
|
|
211
|
+
The database engine to compare with.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, schema: Schema, engine: Engine):
|
|
215
|
+
db_metadata = MetaData()
|
|
216
|
+
with engine.connect() as connection:
|
|
217
|
+
db_metadata.reflect(bind=connection)
|
|
218
|
+
mc = MigrationContext.configure(
|
|
219
|
+
connection, opts={"compare_type": True, "target_metadata": db_metadata}
|
|
220
|
+
)
|
|
221
|
+
schema_metadata = MetaDataBuilder(schema, apply_schema_to_metadata=False).build()
|
|
222
|
+
self.diff = compare_metadata(mc, schema_metadata)
|
|
223
|
+
|
|
224
|
+
def print(self) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Print the differences between the schema and the database.
|
|
227
|
+
"""
|
|
228
|
+
if self.has_changes:
|
|
229
|
+
pprint.pprint(self.diff)
|
felis/metadata.py
CHANGED
|
@@ -125,29 +125,27 @@ class MetaDataBuilder:
|
|
|
125
125
|
The schema object from which to build the SQLAlchemy metadata.
|
|
126
126
|
apply_schema_to_metadata
|
|
127
127
|
Whether to apply the schema name to the metadata object.
|
|
128
|
-
apply_schema_to_tables
|
|
129
|
-
Whether to apply the schema name to the tables.
|
|
130
128
|
ignore_constraints
|
|
131
129
|
Whether to ignore constraints when building the metadata.
|
|
130
|
+
table_name_postfix
|
|
131
|
+
A string to append to the table names when building the metadata.
|
|
132
132
|
"""
|
|
133
133
|
|
|
134
134
|
def __init__(
|
|
135
135
|
self,
|
|
136
136
|
schema: Schema,
|
|
137
137
|
apply_schema_to_metadata: bool = True,
|
|
138
|
-
apply_schema_to_tables: bool = True,
|
|
139
138
|
ignore_constraints: bool = False,
|
|
139
|
+
table_name_postfix: str = "",
|
|
140
140
|
) -> None:
|
|
141
141
|
"""Initialize the metadata builder."""
|
|
142
142
|
self.schema = schema
|
|
143
143
|
if not apply_schema_to_metadata:
|
|
144
144
|
logger.debug("Schema name will not be applied to metadata")
|
|
145
|
-
if not apply_schema_to_tables:
|
|
146
|
-
logger.debug("Schema name will not be applied to tables")
|
|
147
145
|
self.metadata = MetaData(schema=schema.name if apply_schema_to_metadata else None)
|
|
148
146
|
self._objects: dict[str, Any] = {}
|
|
149
|
-
self.apply_schema_to_tables = apply_schema_to_tables
|
|
150
147
|
self.ignore_constraints = ignore_constraints
|
|
148
|
+
self.table_name_postfix = table_name_postfix
|
|
151
149
|
|
|
152
150
|
def build(self) -> MetaData:
|
|
153
151
|
"""Build the SQLAlchemy tables and constraints from the schema.
|
|
@@ -231,11 +229,10 @@ class MetaDataBuilder:
|
|
|
231
229
|
description = table_obj.description
|
|
232
230
|
columns = [self.build_column(column) for column in table_obj.columns]
|
|
233
231
|
table = Table(
|
|
234
|
-
name,
|
|
232
|
+
name + self.table_name_postfix,
|
|
235
233
|
self.metadata,
|
|
236
234
|
*columns,
|
|
237
235
|
comment=description,
|
|
238
|
-
schema=self.schema.name if self.apply_schema_to_tables else None,
|
|
239
236
|
**optargs, # type: ignore[arg-type]
|
|
240
237
|
)
|
|
241
238
|
|
felis/tap_schema.py
CHANGED
|
@@ -91,9 +91,15 @@ class TableManager:
|
|
|
91
91
|
self.table_name_postfix = table_name_postfix
|
|
92
92
|
self.apply_schema_to_metadata = apply_schema_to_metadata
|
|
93
93
|
self.schema_name = schema_name or TableManager._SCHEMA_NAME_STD
|
|
94
|
+
self.table_name_postfix = table_name_postfix
|
|
94
95
|
|
|
95
96
|
if is_valid_engine(engine):
|
|
96
97
|
assert isinstance(engine, Engine)
|
|
98
|
+
if table_name_postfix != "":
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Table name postfix '%s' will be ignored when reflecting TAP_SCHEMA database",
|
|
101
|
+
table_name_postfix,
|
|
102
|
+
)
|
|
97
103
|
logger.debug(
|
|
98
104
|
"Reflecting TAP_SCHEMA database from existing database at %s",
|
|
99
105
|
engine.url._replace(password="***"),
|
|
@@ -133,7 +139,7 @@ class TableManager:
|
|
|
133
139
|
self._metadata = MetaDataBuilder(
|
|
134
140
|
self.schema,
|
|
135
141
|
apply_schema_to_metadata=self.apply_schema_to_metadata,
|
|
136
|
-
|
|
142
|
+
table_name_postfix=self.table_name_postfix,
|
|
137
143
|
).build()
|
|
138
144
|
|
|
139
145
|
logger.debug("Loaded TAP_SCHEMA '%s' from YAML resource", self.schema_name)
|
|
@@ -355,7 +361,7 @@ class TableManager:
|
|
|
355
361
|
engine
|
|
356
362
|
The SQLAlchemy engine to use to create the tables.
|
|
357
363
|
"""
|
|
358
|
-
logger.info("Creating TAP_SCHEMA database '%s'", self.
|
|
364
|
+
logger.info("Creating TAP_SCHEMA database '%s'", self.schema_name)
|
|
359
365
|
self._create_schema(engine)
|
|
360
366
|
self.metadata.create_all(engine)
|
|
361
367
|
|
|
@@ -426,7 +432,7 @@ class DataLoader:
|
|
|
426
432
|
# Execute the inserts if not in dry run mode.
|
|
427
433
|
self._execute_inserts()
|
|
428
434
|
else:
|
|
429
|
-
logger.info("Dry run
|
|
435
|
+
logger.info("Dry run - not loading data into database")
|
|
430
436
|
|
|
431
437
|
def _insert_schemas(self) -> None:
|
|
432
438
|
"""Insert the schema data into the schemas table."""
|
|
@@ -567,7 +573,7 @@ class DataLoader:
|
|
|
567
573
|
def _print_sql(self) -> None:
|
|
568
574
|
"""Print the generated inserts to stdout."""
|
|
569
575
|
for insert_str in self._compiled_inserts():
|
|
570
|
-
print(insert_str)
|
|
576
|
+
print(insert_str + ";")
|
|
571
577
|
|
|
572
578
|
def _write_sql_to_file(self) -> None:
|
|
573
579
|
"""Write the generated insert statements to a file."""
|
|
@@ -575,7 +581,7 @@ class DataLoader:
|
|
|
575
581
|
raise ValueError("No output path specified")
|
|
576
582
|
with open(self.output_path, "w") as outfile:
|
|
577
583
|
for insert_str in self._compiled_inserts():
|
|
578
|
-
outfile.write(insert_str + "\n")
|
|
584
|
+
outfile.write(insert_str + ";" + "\n")
|
|
579
585
|
|
|
580
586
|
def _insert(self, table_name: str, record: list[Any] | dict[str, Any]) -> None:
|
|
581
587
|
"""Generate an insert statement for a record.
|
felis/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "28.
|
|
2
|
+
__version__ = "28.2025.500"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version: 28.
|
|
3
|
+
Version: 28.2025.500
|
|
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
6
|
License: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -13,18 +13,22 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
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:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: click
|
|
24
|
-
Requires-Dist:
|
|
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
|
|
27
|
+
Requires-Dist: lsst-utils
|
|
28
|
+
Requires-Dist: numpy
|
|
29
|
+
Requires-Dist: pydantic<3,>=2
|
|
30
|
+
Requires-Dist: pyyaml
|
|
31
|
+
Requires-Dist: sqlalchemy
|
|
28
32
|
Provides-Extra: test
|
|
29
33
|
Requires-Dist: pytest>=3.2; extra == "test"
|
|
30
34
|
Provides-Extra: dev
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
felis/__init__.py,sha256=r1KFSnc55gziwUuYb9s2EfwrI_85aa3LpaKwk6rUvvs,1108
|
|
2
|
+
felis/cli.py,sha256=Wf-sEUZ-B9zzn4M1huY2ruV1nkgVmpzX8f8iuFfyxZc,14469
|
|
3
|
+
felis/datamodel.py,sha256=NczAA4HBBC4-uxPNsrKAFX-hdlgvCT2qqEJCEqDy4yg,39265
|
|
4
|
+
felis/diff.py,sha256=0N4OcBCzbL9DW_XGAeuvGsQ0zIhq8fY-Kx2QdvLv-Ds,7492
|
|
5
|
+
felis/metadata.py,sha256=cYx_qizkLBqcoxWV46h4TbwTi1KVJAkuA2OuUmD-K5k,13536
|
|
6
|
+
felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
felis/tap_schema.py,sha256=DgHH4hBf4q_F540TAR9GTKcALwUkk8iTw5pzQlmv1DA,22753
|
|
8
|
+
felis/types.py,sha256=m80GSGfNHQ3-NzRuTzKOyRXLJboPxdk9kzpp1SO8XdY,5510
|
|
9
|
+
felis/version.py,sha256=S4DyUuQLyaEbonnBItCg8kyZ-_mgVePitRnB-HX_q2Y,54
|
|
10
|
+
felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
felis/db/dialects.py,sha256=n5La-shu-8fHLIyf8rrazHDyrzATmMCdELtKV_0ymxI,3517
|
|
12
|
+
felis/db/schema.py,sha256=NOFXzBoBQcgpoRlgT3LoC70FKp7pCSmFEJ7rU8FIT-c,2101
|
|
13
|
+
felis/db/sqltypes.py,sha256=JJy97U8KzAOg5pFi2xZgSjvU8CXXgrzkvCsmo6FLRG4,11060
|
|
14
|
+
felis/db/utils.py,sha256=SIl2ryOT2Zn5n0BqdNDxC1HcOoxh0doaKk_hMUGvwAc,14116
|
|
15
|
+
felis/db/variants.py,sha256=eahthrbVeV8ZdGamWQccNmWgx6CCscGrU0vQRs5HZK8,5260
|
|
16
|
+
felis/schemas/tap_schema_std.yaml,sha256=sPW-Vk72nY0PFpCvP5d8L8fWvhkif-x32sGtcfDZ8bU,7131
|
|
17
|
+
felis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
felis/tests/postgresql.py,sha256=B_xk4fLual5-viGDqP20r94okuc0pbSvytRH_L0fvMs,4035
|
|
19
|
+
lsst_felis-28.2025.500.dist-info/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
|
|
20
|
+
lsst_felis-28.2025.500.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
21
|
+
lsst_felis-28.2025.500.dist-info/METADATA,sha256=47DRp1MC63ZQA0CbZR8KWLxoh-dnv6V7iN-gtBO2Xms,1410
|
|
22
|
+
lsst_felis-28.2025.500.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
23
|
+
lsst_felis-28.2025.500.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
|
|
24
|
+
lsst_felis-28.2025.500.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
|
|
25
|
+
lsst_felis-28.2025.500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
26
|
+
lsst_felis-28.2025.500.dist-info/RECORD,,
|