lsst-felis 26.2024.1400__py3-none-any.whl → 26.2024.1600__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/cli.py CHANGED
@@ -373,22 +373,37 @@ def merge(files: Iterable[io.TextIOBase]) -> None:
373
373
  type=click.Choice(["RSP", "default"]),
374
374
  default="default",
375
375
  )
376
- @click.option("-d", "--require-description", is_flag=True, help="Require description for all objects")
376
+ @click.option(
377
+ "-d", "--require-description", is_flag=True, help="Require description for all objects", default=False
378
+ )
379
+ @click.option(
380
+ "-t", "--check-redundant-datatypes", is_flag=True, help="Check for redundant datatypes", default=False
381
+ )
377
382
  @click.argument("files", nargs=-1, type=click.File())
378
- def validate(schema_name: str, require_description: bool, files: Iterable[io.TextIOBase]) -> None:
383
+ def validate(
384
+ schema_name: str,
385
+ require_description: bool,
386
+ check_redundant_datatypes: bool,
387
+ files: Iterable[io.TextIOBase],
388
+ ) -> None:
379
389
  """Validate one or more felis YAML files."""
380
390
  schema_class = get_schema(schema_name)
381
- logger.info(f"Using schema '{schema_class.__name__}'")
382
-
383
- if require_description:
384
- Schema.require_description(True)
391
+ if schema_name != "default":
392
+ logger.info(f"Using schema '{schema_class.__name__}'")
385
393
 
386
394
  rc = 0
387
395
  for file in files:
388
396
  file_name = getattr(file, "name", None)
389
397
  logger.info(f"Validating {file_name}")
390
398
  try:
391
- schema_class.model_validate(yaml.load(file, Loader=yaml.SafeLoader))
399
+ data = yaml.load(file, Loader=yaml.SafeLoader)
400
+ schema_class.model_validate(
401
+ data,
402
+ context={
403
+ "check_redundant_datatypes": check_redundant_datatypes,
404
+ "require_description": require_description,
405
+ },
406
+ )
392
407
  except ValidationError as e:
393
408
  logger.error(e)
394
409
  rc = 1
felis/datamodel.py CHANGED
@@ -22,13 +22,22 @@
22
22
  from __future__ import annotations
23
23
 
24
24
  import logging
25
+ import re
25
26
  from collections.abc import Mapping, Sequence
26
- from enum import Enum
27
+ from enum import StrEnum, auto
27
28
  from typing import Annotated, Any, Literal, TypeAlias
28
29
 
29
30
  from astropy import units as units # type: ignore
30
31
  from astropy.io.votable import ucd # type: ignore
31
- from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
32
+ from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator, model_validator
33
+ from sqlalchemy import dialects
34
+ from sqlalchemy import types as sqa_types
35
+ from sqlalchemy.engine import create_mock_engine
36
+ from sqlalchemy.engine.interfaces import Dialect
37
+ from sqlalchemy.types import TypeEngine
38
+
39
+ from .db.sqltypes import get_type_func
40
+ from .types import FelisType
32
41
 
33
42
  logger = logging.getLogger(__name__)
34
43
 
@@ -49,7 +58,6 @@ __all__ = (
49
58
  CONFIG = ConfigDict(
50
59
  populate_by_name=True, # Populate attributes by name.
51
60
  extra="forbid", # Do not allow extra fields.
52
- validate_assignment=True, # Validate assignments after model is created.
53
61
  str_strip_whitespace=True, # Strip whitespace from string fields.
54
62
  )
55
63
  """Pydantic model configuration as described in:
@@ -83,40 +91,83 @@ class BaseObject(BaseModel):
83
91
  """
84
92
 
85
93
  description: DescriptionStr | None = None
86
- """A description of the database object.
87
-
88
- By default, the description is optional but will be required if
89
- `BaseObject.Config.require_description` is set to `True` by the user.
90
- """
94
+ """A description of the database object."""
91
95
 
92
- @model_validator(mode="before")
96
+ @model_validator(mode="after") # type: ignore[arg-type]
93
97
  @classmethod
94
- def check_description(cls, values: dict[str, Any]) -> dict[str, Any]:
98
+ def check_description(cls, object: BaseObject, info: ValidationInfo) -> BaseObject:
95
99
  """Check that the description is present if required."""
96
- if Schema.is_description_required():
97
- if "description" not in values or not values["description"]:
98
- raise ValueError("Description is required and must be non-empty")
99
- if len(values["description"].strip()) < DESCR_MIN_LENGTH:
100
- raise ValueError(f"Description must be at least {DESCR_MIN_LENGTH} characters long")
101
- return values
100
+ context = info.context
101
+ if not context or not context.get("require_description", False):
102
+ return object
103
+ if object.description is None or object.description == "":
104
+ raise ValueError("Description is required and must be non-empty")
105
+ if len(object.description) < DESCR_MIN_LENGTH:
106
+ raise ValueError(f"Description must be at least {DESCR_MIN_LENGTH} characters long")
107
+ return object
102
108
 
103
109
 
104
- class DataType(Enum):
110
+ class DataType(StrEnum):
105
111
  """`Enum` representing the data types supported by Felis."""
106
112
 
107
- BOOLEAN = "boolean"
108
- BYTE = "byte"
109
- SHORT = "short"
110
- INT = "int"
111
- LONG = "long"
112
- FLOAT = "float"
113
- DOUBLE = "double"
114
- CHAR = "char"
115
- STRING = "string"
116
- UNICODE = "unicode"
117
- TEXT = "text"
118
- BINARY = "binary"
119
- TIMESTAMP = "timestamp"
113
+ boolean = auto()
114
+ byte = auto()
115
+ short = auto()
116
+ int = auto()
117
+ long = auto()
118
+ float = auto()
119
+ double = auto()
120
+ char = auto()
121
+ string = auto()
122
+ unicode = auto()
123
+ text = auto()
124
+ binary = auto()
125
+ timestamp = auto()
126
+
127
+
128
+ _DIALECTS = {
129
+ "mysql": create_mock_engine("mysql://", executor=None).dialect,
130
+ "postgresql": create_mock_engine("postgresql://", executor=None).dialect,
131
+ }
132
+ """Dictionary of dialect names to SQLAlchemy dialects."""
133
+
134
+ _DIALECT_MODULES = {"mysql": getattr(dialects, "mysql"), "postgresql": getattr(dialects, "postgresql")}
135
+ """Dictionary of dialect names to SQLAlchemy dialect modules."""
136
+
137
+ _DATATYPE_REGEXP = re.compile(r"(\w+)(\((.*)\))?")
138
+ """Regular expression to match data types in the form "type(length)"""
139
+
140
+
141
+ def string_to_typeengine(
142
+ type_string: str, dialect: Dialect | None = None, length: int | None = None
143
+ ) -> TypeEngine:
144
+ match = _DATATYPE_REGEXP.search(type_string)
145
+ if not match:
146
+ raise ValueError(f"Invalid type string: {type_string}")
147
+
148
+ type_name, _, params = match.groups()
149
+ if dialect is None:
150
+ type_class = getattr(sqa_types, type_name.upper(), None)
151
+ else:
152
+ try:
153
+ dialect_module = _DIALECT_MODULES[dialect.name]
154
+ except KeyError:
155
+ raise ValueError(f"Unsupported dialect: {dialect}")
156
+ type_class = getattr(dialect_module, type_name.upper(), None)
157
+
158
+ if not type_class:
159
+ raise ValueError(f"Unsupported type: {type_class}")
160
+
161
+ if params:
162
+ params = [int(param) if param.isdigit() else param for param in params.split(",")]
163
+ type_obj = type_class(*params)
164
+ else:
165
+ type_obj = type_class()
166
+
167
+ if hasattr(type_obj, "length") and getattr(type_obj, "length") is None and length is not None:
168
+ type_obj.length = length
169
+
170
+ return type_obj
120
171
 
121
172
 
122
173
  class Column(BaseObject):
@@ -207,6 +258,53 @@ class Column(BaseObject):
207
258
 
208
259
  return values
209
260
 
261
+ @model_validator(mode="after") # type: ignore[arg-type]
262
+ @classmethod
263
+ def validate_datatypes(cls, col: Column, info: ValidationInfo) -> Column:
264
+ """Check for redundant datatypes on columns."""
265
+ context = info.context
266
+ if not context or not context.get("check_redundant_datatypes", False):
267
+ return col
268
+ if all(getattr(col, f"{dialect}:datatype", None) is not None for dialect in _DIALECTS.keys()):
269
+ return col
270
+
271
+ datatype = col.datatype
272
+ length: int | None = col.length or None
273
+
274
+ datatype_func = get_type_func(datatype)
275
+ felis_type = FelisType.felis_type(datatype)
276
+ if felis_type.is_sized:
277
+ if length is not None:
278
+ datatype_obj = datatype_func(length)
279
+ else:
280
+ raise ValueError(f"Length must be provided for sized type '{datatype}' in column '{col.id}'")
281
+ else:
282
+ datatype_obj = datatype_func()
283
+
284
+ for dialect_name, dialect in _DIALECTS.items():
285
+ db_annotation = f"{dialect_name}_datatype"
286
+ if datatype_string := col.model_dump().get(db_annotation):
287
+ db_datatype_obj = string_to_typeengine(datatype_string, dialect, length)
288
+ if datatype_obj.compile(dialect) == db_datatype_obj.compile(dialect):
289
+ raise ValueError(
290
+ "'{}: {}' is the same as 'datatype: {}' in column '{}'".format(
291
+ db_annotation, datatype_string, col.datatype, col.id
292
+ )
293
+ )
294
+ else:
295
+ logger.debug(
296
+ "Valid type override of 'datatype: {}' with '{}: {}' in column '{}'".format(
297
+ col.datatype, db_annotation, datatype_string, col.id
298
+ )
299
+ )
300
+ logger.debug(
301
+ "Compiled datatype '{}' with {} compiled override '{}'".format(
302
+ datatype_obj.compile(dialect), dialect_name, db_datatype_obj.compile(dialect)
303
+ )
304
+ )
305
+
306
+ return col
307
+
210
308
 
211
309
  class Constraint(BaseObject):
212
310
  """A database table constraint."""
@@ -404,15 +502,6 @@ class SchemaIdVisitor:
404
502
  class Schema(BaseObject):
405
503
  """The database schema containing the tables."""
406
504
 
407
- class ValidationConfig:
408
- """Validation configuration which is specific to Felis."""
409
-
410
- _require_description = False
411
- """Flag to require a description for all objects.
412
-
413
- This is set by the `require_description` class method.
414
- """
415
-
416
505
  version: SchemaVersion | str | None = None
417
506
  """The version of the schema."""
418
507
 
@@ -430,21 +519,29 @@ class Schema(BaseObject):
430
519
  raise ValueError("Table names must be unique")
431
520
  return tables
432
521
 
433
- @model_validator(mode="after")
434
- def create_id_map(self: Schema) -> Schema:
435
- """Create a map of IDs to objects."""
522
+ def _create_id_map(self: Schema) -> Schema:
523
+ """Create a map of IDs to objects.
524
+
525
+ This method should not be called by users. It is called automatically
526
+ by the ``model_post_init()`` method. If the ID map is already
527
+ populated, this method will return immediately.
528
+ """
436
529
  if len(self.id_map):
437
- logger.debug("ID map was already populated")
530
+ logger.debug("Ignoring call to create_id_map() - ID map was already populated")
438
531
  return self
439
532
  visitor: SchemaIdVisitor = SchemaIdVisitor()
440
533
  visitor.visit_schema(self)
441
- logger.debug(f"ID map contains {len(self.id_map.keys())} objects")
534
+ logger.debug(f"Created schema ID map with {len(self.id_map.keys())} objects")
442
535
  if len(visitor.duplicates):
443
536
  raise ValueError(
444
537
  "Duplicate IDs found in schema:\n " + "\n ".join(visitor.duplicates) + "\n"
445
538
  )
446
539
  return self
447
540
 
541
+ def model_post_init(self, ctx: Any) -> None:
542
+ """Post-initialization hook for the model."""
543
+ self._create_id_map()
544
+
448
545
  def __getitem__(self, id: str) -> BaseObject:
449
546
  """Get an object by its ID."""
450
547
  if id not in self:
@@ -454,20 +551,3 @@ class Schema(BaseObject):
454
551
  def __contains__(self, id: str) -> bool:
455
552
  """Check if an object with the given ID is in the schema."""
456
553
  return id in self.id_map
457
-
458
- @classmethod
459
- def require_description(cls, rd: bool = True) -> None:
460
- """Set whether a description is required for all objects.
461
-
462
- This includes the schema, tables, columns, and constraints.
463
-
464
- Users should call this method to set the requirement for a description
465
- when validating schemas, rather than change the flag value directly.
466
- """
467
- logger.debug(f"Setting description requirement to '{rd}'")
468
- cls.ValidationConfig._require_description = rd
469
-
470
- @classmethod
471
- def is_description_required(cls) -> bool:
472
- """Return whether a description is required for all objects."""
473
- return cls.ValidationConfig._require_description
felis/db/_variants.py CHANGED
@@ -40,10 +40,10 @@ TABLE_OPTS = {
40
40
  }
41
41
 
42
42
  COLUMN_VARIANT_OVERRIDE = {
43
- "mysql:datatype": "mysql",
44
- "oracle:datatype": "oracle",
45
- "postgresql:datatype": "postgresql",
46
- "sqlite:datatype": "sqlite",
43
+ "mysql_datatype": "mysql",
44
+ "oracle_datatype": "oracle",
45
+ "postgresql_datatype": "postgresql",
46
+ "sqlite_datatype": "sqlite",
47
47
  }
48
48
 
49
49
  DIALECT_MODULES = {MYSQL: mysql, ORACLE: oracle, SQLITE: sqlite, POSTGRES: postgresql}
@@ -87,7 +87,7 @@ def make_variant_dict(column_obj: Column) -> dict[str, TypeEngine[Any]]:
87
87
  """
88
88
  variant_dict = {}
89
89
  for field_name, value in iter(column_obj):
90
- if field_name in COLUMN_VARIANT_OVERRIDE:
90
+ if field_name in COLUMN_VARIANT_OVERRIDE and value is not None:
91
91
  dialect = COLUMN_VARIANT_OVERRIDE[field_name]
92
92
  variant: TypeEngine = process_variant_override(dialect, value)
93
93
  variant_dict[dialect] = variant
felis/db/sqltypes.py CHANGED
@@ -21,9 +21,9 @@
21
21
 
22
22
  import builtins
23
23
  from collections.abc import Mapping
24
- from typing import Any
24
+ from typing import Any, Callable
25
25
 
26
- from sqlalchemy import Float, SmallInteger, types
26
+ from sqlalchemy import SmallInteger, types
27
27
  from sqlalchemy.dialects import mysql, oracle, postgresql
28
28
  from sqlalchemy.ext.compiler import compiles
29
29
 
@@ -39,24 +39,12 @@ class TINYINT(SmallInteger):
39
39
  __visit_name__ = "TINYINT"
40
40
 
41
41
 
42
- class DOUBLE(Float):
43
- """The non-standard DOUBLE type."""
44
-
45
- __visit_name__ = "DOUBLE"
46
-
47
-
48
42
  @compiles(TINYINT)
49
43
  def compile_tinyint(type_: Any, compiler: Any, **kw: Any) -> str:
50
44
  """Return type name for TINYINT."""
51
45
  return "TINYINT"
52
46
 
53
47
 
54
- @compiles(DOUBLE)
55
- def compile_double(type_: Any, compiler: Any, **kw: Any) -> str:
56
- """Return type name for double precision type."""
57
- return "DOUBLE"
58
-
59
-
60
48
  _TypeMap = Mapping[str, types.TypeEngine | type[types.TypeEngine]]
61
49
 
62
50
  boolean_map: _TypeMap = {MYSQL: mysql.BIT(1), ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
@@ -160,7 +148,7 @@ def float(**kwargs: Any) -> types.TypeEngine:
160
148
 
161
149
  def double(**kwargs: Any) -> types.TypeEngine:
162
150
  """Return SQLAlchemy type for double precision float."""
163
- return _vary(DOUBLE(), double_map, kwargs)
151
+ return _vary(types.DOUBLE(), double_map, kwargs)
164
152
 
165
153
 
166
154
  def char(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
@@ -178,9 +166,9 @@ def unicode(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
178
166
  return _vary(types.NVARCHAR(length), unicode_map, kwargs, length)
179
167
 
180
168
 
181
- def text(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
169
+ def text(**kwargs: Any) -> types.TypeEngine:
182
170
  """Return SQLAlchemy type for text."""
183
- return _vary(types.CLOB(length), text_map, kwargs, length)
171
+ return _vary(types.TEXT(), text_map, kwargs)
184
172
 
185
173
 
186
174
  def binary(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
@@ -193,6 +181,13 @@ def timestamp(**kwargs: Any) -> types.TypeEngine:
193
181
  return types.TIMESTAMP()
194
182
 
195
183
 
184
+ def get_type_func(type_name: str) -> Callable:
185
+ """Return the function for the type with the given name."""
186
+ if type_name not in globals():
187
+ raise ValueError(f"Unknown type: {type_name}")
188
+ return globals()[type_name]
189
+
190
+
196
191
  def _vary(
197
192
  type_: types.TypeEngine,
198
193
  variant_map: _TypeMap,
@@ -203,7 +198,7 @@ def _vary(
203
198
  variants.update(overrides)
204
199
  for dialect, variant in variants.items():
205
200
  # If this is a class and not an instance, instantiate
206
- if isinstance(variant, type):
201
+ if callable(variant):
207
202
  variant = variant(*args)
208
203
  type_ = type_.with_variant(variant, dialect)
209
204
  return type_
felis/types.py CHANGED
@@ -125,7 +125,7 @@ class Unicode(FelisType, felis_name="unicode", votable_name="unicodeChar", is_si
125
125
  """Felis definition of unicode string type."""
126
126
 
127
127
 
128
- class Text(FelisType, felis_name="text", votable_name="unicodeChar", is_sized=True):
128
+ class Text(FelisType, felis_name="text", votable_name="char"):
129
129
  """Felis definition of text type."""
130
130
 
131
131
 
felis/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "26.2024.1400"
2
+ __version__ = "26.2024.1600"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lsst-felis
3
- Version: 26.2024.1400
3
+ Version: 26.2024.1600
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+)
@@ -0,0 +1,24 @@
1
+ felis/__init__.py,sha256=_Pw-QKMYj0WRgE8fW2N2pBXJUj-Pjv8dSKJBzykjyZU,1842
2
+ felis/check.py,sha256=RBxXq7XwPGIucrs1PPgPtgk8MrWAJlOmoxCNySEz9-I,13892
3
+ felis/cli.py,sha256=IG_G41LRcEa7Y_6iFQ4i44LmyARhiqiv_0gFvLeJzpA,17041
4
+ felis/datamodel.py,sha256=Wv7SAOTahIMWWpYFT_VxBD4WDRYlcm83SGAAy30Ly0c,19293
5
+ felis/metadata.py,sha256=5DE2YMnu6YuhwntBSe-OheCD7C2-vA4yb64BpjTC68A,18542
6
+ felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ felis/simple.py,sha256=yzv_aoZrZhfakd1Xm7gLDeVKyJjCDZ7wAyYYp-l_Sxs,14414
8
+ felis/tap.py,sha256=RBwEKyU3S0oXSNIMoI2WRAuC9WB5eai9BdQQUYN5Qdc,17704
9
+ felis/types.py,sha256=z_ECfSxpqiFSGppjxKwCO4fPP7TLBaIN3Qo1AGF16Go,4418
10
+ felis/utils.py,sha256=tYxr0xFdPN4gDHibeAD9d5DFgU8hKlSZVKmZoDzi4e8,4164
11
+ felis/validation.py,sha256=f9VKvp7q-cnim2D5voTKwCdt0NRsYBpTwom1Z_3OKkc,3469
12
+ felis/version.py,sha256=H15ZOvw6LpBJgcJl5X5QV2o57W94MWuGRM3CPP_z0Xk,55
13
+ felis/visitor.py,sha256=EazU4nYbkKBj3mCZYvsTCBTNmh0qRaUNZIzCcM3dqOQ,6439
14
+ felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ felis/db/_variants.py,sha256=zCuXDgU_x_pTZcWkBLgqQCiOhlA6y2tBt-PUQfafwmM,3368
16
+ felis/db/sqltypes.py,sha256=-YPkU5yS7udcPZTg__G_85tH6oMqVm6hEcHUgYpgiT8,5734
17
+ lsst_felis-26.2024.1600.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
18
+ lsst_felis-26.2024.1600.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
19
+ lsst_felis-26.2024.1600.dist-info/METADATA,sha256=7F-m_tITJUAB08GAsCdFA2b7kmga_AZqtk8-B0uTQ3g,1101
20
+ lsst_felis-26.2024.1600.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
21
+ lsst_felis-26.2024.1600.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
22
+ lsst_felis-26.2024.1600.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
23
+ lsst_felis-26.2024.1600.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
+ lsst_felis-26.2024.1600.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- felis/__init__.py,sha256=_Pw-QKMYj0WRgE8fW2N2pBXJUj-Pjv8dSKJBzykjyZU,1842
2
- felis/check.py,sha256=RBxXq7XwPGIucrs1PPgPtgk8MrWAJlOmoxCNySEz9-I,13892
3
- felis/cli.py,sha256=YeGSiA3ywPVMMdB1YxH1_Gdac1kl4oPJvJtajfCs5VU,16637
4
- felis/datamodel.py,sha256=ooNSg68OuNk89EVu1MtxupLUWgSyzmb50wjza1joDO4,16002
5
- felis/metadata.py,sha256=5DE2YMnu6YuhwntBSe-OheCD7C2-vA4yb64BpjTC68A,18542
6
- felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- felis/simple.py,sha256=yzv_aoZrZhfakd1Xm7gLDeVKyJjCDZ7wAyYYp-l_Sxs,14414
8
- felis/tap.py,sha256=RBwEKyU3S0oXSNIMoI2WRAuC9WB5eai9BdQQUYN5Qdc,17704
9
- felis/types.py,sha256=1GL6IkHcIsIydiyw1eX98REh-lWCVIRO-9qjmaZfqvw,4440
10
- felis/utils.py,sha256=tYxr0xFdPN4gDHibeAD9d5DFgU8hKlSZVKmZoDzi4e8,4164
11
- felis/validation.py,sha256=f9VKvp7q-cnim2D5voTKwCdt0NRsYBpTwom1Z_3OKkc,3469
12
- felis/version.py,sha256=Bkf7vQojVYVWgjqmEFJRzUA7IlOUtWz7QH7SQ12F5YA,55
13
- felis/visitor.py,sha256=EazU4nYbkKBj3mCZYvsTCBTNmh0qRaUNZIzCcM3dqOQ,6439
14
- felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- felis/db/_variants.py,sha256=aW0Q7R4KEtxLR7VMashQjDLWdzDNrMVAH521MSvMey0,3346
16
- felis/db/sqltypes.py,sha256=0HOEqvL0OailGP-j6Jj5tnOSu_Pt7Hi29PPof4Q5d2c,5787
17
- lsst_felis-26.2024.1400.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
18
- lsst_felis-26.2024.1400.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
19
- lsst_felis-26.2024.1400.dist-info/METADATA,sha256=y4cVRA28EHR2jiWXFDDzSRCm16TH4YnJrept5w3fkMM,1101
20
- lsst_felis-26.2024.1400.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
21
- lsst_felis-26.2024.1400.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
22
- lsst_felis-26.2024.1400.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
23
- lsst_felis-26.2024.1400.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
- lsst_felis-26.2024.1400.dist-info/RECORD,,