lsst-felis 29.2025.4500__py3-none-any.whl → 30.0.0rc3__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.
@@ -0,0 +1,31 @@
1
+ felis/__init__.py,sha256=j7SDnTb0u0ElxDaJDNSeEznpqvxR1Az4Af8nESsDcDs,1148
2
+ felis/cli.py,sha256=BHZ75V0PupuAAAGzHPPb8j3G8iueC_vA9wJw5KV0kOM,19431
3
+ felis/datamodel.py,sha256=4AEyP9cYpIvuJIABBzsOm8iQuEsV31dZOJG4vjEBq8w,52116
4
+ felis/diff.py,sha256=ZzjOJ57p5ZwFn6eem7CYoPjSnxti5OZY33B6Ds5Q-Rg,7797
5
+ felis/metadata.py,sha256=WRtwx4lA8n6r8LbeNSMwvRzHuv22oGuttGlHsm_qJt0,16157
6
+ felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ felis/tap_schema.py,sha256=X_5LD_T78Ga9OvKBBbDDJcLA6DpXwIld8no84WYtYPs,26736
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_extensions.yaml,sha256=abNqmjW8hVUBLAXMcNt_VmZ8wPikz9N32epxbBApTdU,2164
15
+ felis/config/tap_schema/tap_schema_std.yaml,sha256=sPW-Vk72nY0PFpCvP5d8L8fWvhkif-x32sGtcfDZ8bU,7131
16
+ felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ felis/db/_dialects.py,sha256=Y7FV0nI4Vm-fQG3oR71CZor09DKN1QEp9rkOwwvwzuM,5601
18
+ felis/db/_sqltypes.py,sha256=Q2p3Af3O5-B1ZxQ4M2j_w8SH1o_kp6ezg8h7LmSlfww,11060
19
+ felis/db/_variants.py,sha256=Vy5s8lF80WErADKsCwgMvlET--4Q8NW4lK9dSltH8nc,5261
20
+ felis/db/database_context.py,sha256=VHjOZ71r-HbcCijq4WG2rw1r0sYeGQTEJ5SgjBfRSrM,29270
21
+ felis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ felis/tests/postgresql.py,sha256=JtUgNaM54e5ktcWF-NE4p4B6eeJRpVjz0QVJogTd3eg,4035
23
+ felis/tests/run_cli.py,sha256=Gg8loUIGj9t6KlkRKrEc9Z9b5dtlkpJy94ORuj4BrxU,2503
24
+ lsst_felis-30.0.0rc3.dist-info/licenses/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
25
+ lsst_felis-30.0.0rc3.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
26
+ lsst_felis-30.0.0rc3.dist-info/METADATA,sha256=IXBalUeGwBneWF2X5HFbG7KSQwNr4DqJyJz8bKF9vKk,1374
27
+ lsst_felis-30.0.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ lsst_felis-30.0.0rc3.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
29
+ lsst_felis-30.0.0rc3.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
30
+ lsst_felis-30.0.0rc3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
+ lsst_felis-30.0.0rc3.dist-info/RECORD,,
felis/db/schema.py DELETED
@@ -1,62 +0,0 @@
1
- """Database utilities for Felis schemas."""
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
- from sqlalchemy import Engine, create_engine
25
-
26
- from ..datamodel import Schema
27
- from ..metadata import MetaDataBuilder
28
- from .utils import DatabaseContext
29
-
30
- __all__ = ["create_database"]
31
-
32
-
33
- def create_database(schema: Schema, engine_or_url_str: Engine | str | None = None) -> DatabaseContext:
34
- """
35
- Create a database from the specified `Schema`.
36
-
37
- Parameters
38
- ----------
39
- schema
40
- The schema to create.
41
- engine_or_url_str
42
- The SQLAlchemy engine or URL to use for database creation.
43
- If None, an in-memory SQLite database will be created.
44
-
45
- Returns
46
- -------
47
- `DatabaseContext`
48
- The database context object.
49
- """
50
- if engine_or_url_str is not None:
51
- engine = (
52
- engine_or_url_str if isinstance(engine_or_url_str, Engine) else create_engine(engine_or_url_str)
53
- )
54
- else:
55
- engine = create_engine("sqlite:///:memory:")
56
- metadata = MetaDataBuilder(
57
- schema, apply_schema_to_metadata=False if engine.url.drivername == "sqlite" else True
58
- ).build()
59
- ctx = DatabaseContext(metadata, engine)
60
- ctx.initialize()
61
- ctx.create_all()
62
- return ctx
felis/db/utils.py DELETED
@@ -1,409 +0,0 @@
1
- """Database utility functions and classes."""
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
- from __future__ import annotations
25
-
26
- import logging
27
- import re
28
- from typing import IO, Any
29
-
30
- from sqlalchemy import MetaData, types
31
- from sqlalchemy.engine import Dialect, Engine, ResultProxy
32
- from sqlalchemy.engine.mock import MockConnection, create_mock_engine
33
- from sqlalchemy.engine.url import URL
34
- from sqlalchemy.exc import SQLAlchemyError
35
- from sqlalchemy.schema import CreateSchema, DropSchema
36
- from sqlalchemy.sql import text
37
- from sqlalchemy.types import TypeEngine
38
-
39
- from .dialects import get_dialect_module
40
-
41
- __all__ = ["ConnectionWrapper", "DatabaseContext", "SQLWriter", "string_to_typeengine"]
42
-
43
- logger = logging.getLogger("felis")
44
-
45
- _DATATYPE_REGEXP = re.compile(r"(\w+)(\((.*)\))?")
46
- """Regular expression to match data types with parameters in parentheses."""
47
-
48
-
49
- def string_to_typeengine(
50
- type_string: str, dialect: Dialect | None = None, length: int | None = None
51
- ) -> TypeEngine:
52
- """Convert a string representation of a datatype to a SQLAlchemy type.
53
-
54
- Parameters
55
- ----------
56
- type_string
57
- The string representation of the data type.
58
- dialect
59
- The SQLAlchemy dialect to use. If None, the default dialect will be
60
- used.
61
- length
62
- The length of the data type. If the data type does not have a length
63
- attribute, this parameter will be ignored.
64
-
65
- Returns
66
- -------
67
- `sqlalchemy.types.TypeEngine`
68
- The SQLAlchemy type engine object.
69
-
70
- Raises
71
- ------
72
- ValueError
73
- Raised if the type string is invalid or the type is not supported.
74
-
75
- Notes
76
- -----
77
- This function is used when converting type override strings defined in
78
- fields such as ``mysql:datatype`` in the schema data.
79
- """
80
- match = _DATATYPE_REGEXP.search(type_string)
81
- if not match:
82
- raise ValueError(f"Invalid type string: {type_string}")
83
-
84
- type_name, _, params = match.groups()
85
- if dialect is None:
86
- type_class = getattr(types, type_name.upper(), None)
87
- else:
88
- try:
89
- dialect_module = get_dialect_module(dialect.name)
90
- except KeyError:
91
- raise ValueError(f"Unsupported dialect: {dialect}")
92
- type_class = getattr(dialect_module, type_name.upper(), None)
93
-
94
- if not type_class:
95
- raise ValueError(f"Unsupported type: {type_class}")
96
-
97
- if params:
98
- params = [int(param) if param.isdigit() else param for param in params.split(",")]
99
- type_obj = type_class(*params)
100
- else:
101
- type_obj = type_class()
102
-
103
- if hasattr(type_obj, "length") and getattr(type_obj, "length") is None and length is not None:
104
- type_obj.length = length
105
-
106
- return type_obj
107
-
108
-
109
- def is_mock_url(url: URL) -> bool:
110
- """Check if the engine URL is a mock URL.
111
-
112
- Parameters
113
- ----------
114
- url
115
- The SQLAlchemy engine URL.
116
-
117
- Returns
118
- -------
119
- bool
120
- True if the URL is a mock URL, False otherwise.
121
- """
122
- return (url.drivername == "sqlite" and url.database is None) or (
123
- url.drivername != "sqlite" and url.host is None
124
- )
125
-
126
-
127
- def is_valid_engine(engine: Engine | MockConnection | None) -> bool:
128
- """Check if the engine is valid.
129
-
130
- The engine cannot be none; it must not be a mock connection; and it must
131
- not be a mock URL which is missing a host or, for sqlite, a database name.
132
-
133
- Parameters
134
- ----------
135
- engine
136
- The SQLAlchemy engine or mock connection.
137
-
138
- Returns
139
- -------
140
- bool
141
- True if the engine is valid, False otherwise.
142
- """
143
- return engine is not None and not isinstance(engine, MockConnection) and not is_mock_url(engine.url)
144
-
145
-
146
- class SQLWriter:
147
- """Write SQL statements to stdout or a file.
148
-
149
- Parameters
150
- ----------
151
- file
152
- The file to write the SQL statements to. If None, the statements
153
- will be written to stdout.
154
- """
155
-
156
- def __init__(self, file: IO[str] | None = None) -> None:
157
- """Initialize the SQL writer."""
158
- self.file = file
159
- self.dialect: Dialect | None = None
160
-
161
- def write(self, sql: Any, *multiparams: Any, **params: Any) -> None:
162
- """Write the SQL statement to a file or stdout.
163
-
164
- Statements with parameters will be formatted with the values
165
- inserted into the resultant SQL output.
166
-
167
- Parameters
168
- ----------
169
- sql
170
- The SQL statement to write.
171
- *multiparams
172
- The multiparams to use for the SQL statement.
173
- **params
174
- The params to use for the SQL statement.
175
-
176
- Notes
177
- -----
178
- The functions arguments are typed very loosely because this method in
179
- SQLAlchemy is untyped, amd we do not call it directly.
180
- """
181
- compiled = sql.compile(dialect=self.dialect)
182
- sql_str = str(compiled) + ";"
183
- params_list = [compiled.params]
184
- for params in params_list:
185
- if not params:
186
- print(sql_str, file=self.file)
187
- continue
188
- new_params = {}
189
- for key, value in params.items():
190
- if isinstance(value, str):
191
- new_params[key] = f"'{value}'"
192
- elif value is None:
193
- new_params[key] = "null"
194
- else:
195
- new_params[key] = value
196
- print(sql_str % new_params, file=self.file)
197
-
198
-
199
- class ConnectionWrapper:
200
- """Wrap a SQLAlchemy engine or mock connection to provide a consistent
201
- interface for executing SQL statements.
202
-
203
- Parameters
204
- ----------
205
- engine
206
- The SQLAlchemy engine or mock connection to wrap.
207
- """
208
-
209
- def __init__(self, engine: Engine | MockConnection):
210
- """Initialize the connection wrapper."""
211
- self.engine = engine
212
-
213
- def execute(self, statement: Any) -> ResultProxy:
214
- """Execute a SQL statement on the engine and return the result.
215
-
216
- Parameters
217
- ----------
218
- statement
219
- The SQL statement to execute.
220
-
221
- Returns
222
- -------
223
- ``sqlalchemy.engine.ResultProxy``
224
- The result of the statement execution.
225
-
226
- Notes
227
- -----
228
- The statement will be executed in a transaction block if not using
229
- a mock connection.
230
- """
231
- if isinstance(statement, str):
232
- statement = text(statement)
233
- if isinstance(self.engine, Engine):
234
- try:
235
- with self.engine.begin() as connection:
236
- result = connection.execute(statement)
237
- return result
238
- except SQLAlchemyError as e:
239
- connection.rollback()
240
- logger.error(f"Error executing statement: {e}")
241
- raise
242
- elif isinstance(self.engine, MockConnection):
243
- return self.engine.connect().execute(statement)
244
- else:
245
- raise ValueError("Unsupported engine type:" + str(type(self.engine)))
246
-
247
-
248
- class DatabaseContext:
249
- """Manage the database connection and SQLAlchemy metadata.
250
-
251
- Parameters
252
- ----------
253
- metadata
254
- The SQLAlchemy metadata object.
255
-
256
- engine
257
- The SQLAlchemy engine or mock connection object.
258
- """
259
-
260
- def __init__(self, metadata: MetaData, engine: Engine | MockConnection):
261
- """Initialize the database context."""
262
- self.engine = engine
263
- self.dialect_name = engine.dialect.name
264
- self.metadata = metadata
265
- self.connection = ConnectionWrapper(engine)
266
-
267
- def initialize(self) -> None:
268
- """Create the schema in the database if it does not exist.
269
-
270
- Raises
271
- ------
272
- ValueError
273
- Raised if the database is not supported or it already exists.
274
- sqlalchemy.exc.SQLAlchemyError
275
- Raised if there is an error creating the schema.
276
-
277
- Notes
278
- -----
279
- In MySQL, this will create a new database and, in PostgreSQL, it will
280
- create a new schema. For other variants, this is an unsupported
281
- operation.
282
- """
283
- if self.engine.dialect.name == "sqlite":
284
- # Initialization is unneeded for sqlite.
285
- return
286
- schema_name = self.metadata.schema
287
- if schema_name is None:
288
- raise ValueError("Schema name is required to initialize the schema.")
289
- try:
290
- if self.dialect_name == "mysql":
291
- logger.debug(f"Checking if MySQL database exists: {schema_name}")
292
- result = self.execute(text(f"SHOW DATABASES LIKE '{schema_name}'"))
293
- if result.fetchone():
294
- raise ValueError(f"MySQL database '{schema_name}' already exists.")
295
- logger.debug(f"Creating MySQL database: {schema_name}")
296
- self.execute(text(f"CREATE DATABASE {schema_name}"))
297
- elif self.dialect_name == "postgresql":
298
- logger.debug(f"Checking if PG schema exists: {schema_name}")
299
- result = self.execute(
300
- text(
301
- f"""
302
- SELECT schema_name
303
- FROM information_schema.schemata
304
- WHERE schema_name = '{schema_name}'
305
- """
306
- )
307
- )
308
- if result.fetchone():
309
- raise ValueError(f"PostgreSQL schema '{schema_name}' already exists.")
310
- logger.debug(f"Creating PG schema: {schema_name}")
311
- self.execute(CreateSchema(schema_name))
312
- else:
313
- raise ValueError(f"Initialization not supported for: {self.dialect_name}")
314
- except SQLAlchemyError as e:
315
- logger.error(f"Error creating schema: {e}")
316
- raise
317
-
318
- def drop(self) -> None:
319
- """Drop the schema in the database if it exists.
320
-
321
- Raises
322
- ------
323
- ValueError
324
- Raised if the database is not supported.
325
-
326
- Notes
327
- -----
328
- In MySQL, this will drop a database. In PostgreSQL, it will drop a
329
- schema. A SQlite database will have all its tables dropped. For other
330
- database variants, this is currently an unsupported operation.
331
- """
332
- try:
333
- if self.dialect_name == "sqlite":
334
- if isinstance(self.engine, Engine):
335
- logger.debug("Dropping tables in SQLite schema")
336
- self.metadata.drop_all(bind=self.engine)
337
- else:
338
- schema_name = self.metadata.schema
339
- if schema_name is None:
340
- raise ValueError("Schema name is required to drop the schema.")
341
- if self.dialect_name == "mysql":
342
- logger.debug(f"Dropping MySQL database if exists: {schema_name}")
343
- self.execute(text(f"DROP DATABASE IF EXISTS {schema_name}"))
344
- elif self.dialect_name == "postgresql":
345
- logger.debug(f"Dropping PostgreSQL schema if exists: {schema_name}")
346
- self.execute(DropSchema(schema_name, if_exists=True, cascade=True))
347
- except SQLAlchemyError as e:
348
- logger.error(f"Error dropping schema: {e}")
349
- raise
350
-
351
- def create_all(self) -> None:
352
- """Create all tables in the schema using the metadata object."""
353
- if isinstance(self.engine, Engine):
354
- # Use a transaction for a real connection.
355
- with self.engine.begin() as conn:
356
- try:
357
- self.metadata.create_all(bind=conn)
358
- conn.commit()
359
- except SQLAlchemyError as e:
360
- conn.rollback()
361
- logger.error(f"Error creating tables: {e}")
362
- raise
363
- elif isinstance(self.engine, MockConnection):
364
- # Mock connection so no need for a transaction.
365
- self.metadata.create_all(self.engine)
366
- else:
367
- raise ValueError("Unsupported engine type: " + str(type(self.engine)))
368
-
369
- @staticmethod
370
- def create_mock_engine(engine_url: str | URL, output_file: IO[str] | None = None) -> MockConnection:
371
- """Create a mock engine for testing or dumping DDL statements.
372
-
373
- Parameters
374
- ----------
375
- engine_url
376
- The SQLAlchemy engine URL.
377
- output_file
378
- The file to write the SQL statements to. If None, the statements
379
- will be written to stdout.
380
-
381
- Returns
382
- -------
383
- ``sqlalchemy.engine.mock.MockConnection``
384
- The mock connection object.
385
- """
386
- writer = SQLWriter(output_file)
387
- engine = create_mock_engine(engine_url, executor=writer.write, paramstyle="pyformat")
388
- writer.dialect = engine.dialect
389
- return engine
390
-
391
- def execute(self, statement: Any) -> ResultProxy:
392
- """Execute a SQL statement on the engine and return the result.
393
-
394
- Parameters
395
- ----------
396
- statement
397
- The SQL statement to execute.
398
-
399
- Returns
400
- -------
401
- ``sqlalchemy.engine.ResultProxy``
402
- The result of the statement execution.
403
-
404
- Notes
405
- -----
406
- This is just a wrapper around the execution method of the connection
407
- object, which may execute on a real or mock connection.
408
- """
409
- return self.connection.execute(statement)
@@ -1,31 +0,0 @@
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,,
File without changes