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/__init__.py +12 -1
- felis/cli.py +189 -175
- felis/config/tap_schema/columns.csv +33 -0
- felis/config/tap_schema/key_columns.csv +8 -0
- felis/config/tap_schema/keys.csv +8 -0
- felis/config/tap_schema/schemas.csv +2 -0
- felis/config/tap_schema/tables.csv +6 -0
- felis/datamodel.py +597 -56
- felis/db/dialects.py +1 -1
- felis/db/schema.py +62 -0
- felis/db/sqltypes.py +7 -7
- felis/db/utils.py +1 -1
- felis/diff.py +234 -0
- felis/metadata.py +10 -8
- felis/tap_schema.py +149 -26
- felis/tests/run_cli.py +79 -0
- felis/types.py +7 -7
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info}/METADATA +20 -16
- lsst_felis-29.2025.4500.dist-info/RECORD +31 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info}/WHEEL +1 -1
- felis/tap.py +0 -597
- felis/tests/utils.py +0 -122
- felis/version.py +0 -2
- lsst_felis-28.2024.4500.dist-info/RECORD +0 -26
- /felis/{schemas → config/tap_schema}/tap_schema_std.yaml +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info}/entry_points.txt +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info/licenses}/LICENSE +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info}/top_level.txt +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-29.2025.4500.dist-info}/zip-safe +0 -0
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__ = ["
|
|
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
|
-
|
|
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
|
|
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__), "
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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":
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
"
|
|
29
|
+
"Binary",
|
|
30
30
|
"Boolean",
|
|
31
31
|
"Byte",
|
|
32
|
-
"
|
|
32
|
+
"Char",
|
|
33
|
+
"Double",
|
|
34
|
+
"FelisType",
|
|
35
|
+
"Float",
|
|
33
36
|
"Int",
|
|
34
37
|
"Long",
|
|
35
|
-
"
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version:
|
|
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:
|
|
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:
|
|
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
|
|
28
|
-
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
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
|
|
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,,
|