sqlcrucible 0.4.0__tar.gz → 0.4.1__tar.gz

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.
Files changed (127) hide show
  1. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/PKG-INFO +1 -1
  2. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_version.py +2 -2
  3. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/core.py +16 -9
  4. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_generic_sti.py +31 -0
  5. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/.github/workflows/ci.yml +0 -0
  6. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/.github/workflows/docs.yml +0 -0
  7. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/.gitignore +0 -0
  8. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/.python-version +0 -0
  9. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/LICENSE +0 -0
  10. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/README.md +0 -0
  11. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/comparison.md +0 -0
  12. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/getting-started.md +0 -0
  13. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/advanced.md +0 -0
  14. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/defining-entities.md +0 -0
  15. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/field-mapping.md +0 -0
  16. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/inheritance.md +0 -0
  17. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/orm-descriptors.md +0 -0
  18. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/relationships.md +0 -0
  19. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/guide/type-conversion.md +0 -0
  20. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/index.md +0 -0
  21. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/docs/reference/api.md +0 -0
  22. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/mkdocs.yml +0 -0
  23. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/noxfile.py +0 -0
  24. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/pyproject.toml +0 -0
  25. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/__init__.py +0 -0
  26. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_types/__init__.py +0 -0
  27. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_types/annotations.py +0 -0
  28. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_types/forward_refs.py +0 -0
  29. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_types/match.py +0 -0
  30. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_types/params.py +0 -0
  31. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/_types/transformer.py +0 -0
  32. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/__init__.py +0 -0
  33. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/caching.py +0 -0
  34. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/context.py +0 -0
  35. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/dicts.py +0 -0
  36. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/exceptions.py +0 -0
  37. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/function.py +0 -0
  38. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/literals.py +0 -0
  39. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/noop.py +0 -0
  40. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/registry.py +0 -0
  41. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/sequences.py +0 -0
  42. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/unions.py +0 -0
  43. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/conversion/unwrap.py +0 -0
  44. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/__init__.py +0 -0
  45. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/annotations.py +0 -0
  46. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/automodel.py +0 -0
  47. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/column_projection.py +0 -0
  48. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/descriptors.py +0 -0
  49. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/field_definitions.py +0 -0
  50. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/field_resolution.py +0 -0
  51. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/sa_conversion.py +0 -0
  52. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/entity/sa_type.py +0 -0
  53. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/stubs/__init__.py +0 -0
  54. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/stubs/__main__.py +0 -0
  55. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/stubs/codegen.py +0 -0
  56. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/stubs/discovery.py +0 -0
  57. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/src/sqlcrucible/stubs/serialization.py +0 -0
  58. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/__init__.py +0 -0
  59. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/_types/__init__.py +0 -0
  60. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/_types/test_annotations.py +0 -0
  61. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/_types/test_annotations_properties.py +0 -0
  62. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/_types/test_forward_refs.py +0 -0
  63. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/_types/test_params.py +0 -0
  64. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/__init__.py +0 -0
  65. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/conftest.py +0 -0
  66. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_caching.py +0 -0
  67. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_caching_properties.py +0 -0
  68. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_dicts.py +0 -0
  69. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_literals.py +0 -0
  70. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_literals_properties.py +0 -0
  71. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_noop.py +0 -0
  72. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_registry.py +0 -0
  73. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_sequences.py +0 -0
  74. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_unions.py +0 -0
  75. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/conversion/test_unwrap.py +0 -0
  76. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/__init__.py +0 -0
  77. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/composite/__init__.py +0 -0
  78. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/composite/conftest.py +0 -0
  79. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/composite/test_from_sa_model.py +0 -0
  80. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/composite/test_projections.py +0 -0
  81. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/composite/test_to_column_dict.py +0 -0
  82. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/composite/test_to_sa_model.py +0 -0
  83. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/generic_sti_models.py +0 -0
  84. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/orm_descriptors/__init__.py +0 -0
  85. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/orm_descriptors/conftest.py +0 -0
  86. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/orm_descriptors/test_association_proxy.py +0 -0
  87. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/orm_descriptors/test_hybrid_property.py +0 -0
  88. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/orm_descriptors/test_writable_descriptors.py +0 -0
  89. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_attrs_entity.py +0 -0
  90. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_concrete_table_inheritance.py +0 -0
  91. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_conversion_context.py +0 -0
  92. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_custom_sa_model.py +0 -0
  93. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_dataclass_entity.py +0 -0
  94. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_explicit_table.py +0 -0
  95. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_generic_entity.py +0 -0
  96. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_joined_table_inheritance.py +0 -0
  97. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_pydantic_entity.py +0 -0
  98. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_readonly_field_serialisation.py +0 -0
  99. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_annotated_metadata.py +0 -0
  100. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_back_populates.py +0 -0
  101. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_cycles.py +0 -0
  102. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_eager.py +0 -0
  103. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_many_to_many.py +0 -0
  104. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_one_to_many_child.py +0 -0
  105. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_one_to_many_parent.py +0 -0
  106. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_one_to_one.py +0 -0
  107. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_relationships_self_referential.py +0 -0
  108. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_sa_type.py +0 -0
  109. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/entity/test_single_table_inheritance.py +0 -0
  110. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/strategies.py +0 -0
  111. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/__init__.py +0 -0
  112. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/conftest.py +0 -0
  113. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/sample_models.py +0 -0
  114. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_build_import_block.py +0 -0
  115. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_codegen.py +0 -0
  116. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_construct_model_def.py +0 -0
  117. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_discovery.py +0 -0
  118. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_generate_model_defs.py +0 -0
  119. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_sa_field_type.py +0 -0
  120. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_serialization.py +0 -0
  121. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_stub_generation.py +0 -0
  122. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_typecheck_columns.py +0 -0
  123. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_typecheck_entity_preservation.py +0 -0
  124. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_typecheck_excluded_fields.py +0 -0
  125. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_typecheck_relationships.py +0 -0
  126. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/tests/stubs/test_typecheck_sa_type.py +0 -0
  127. {sqlcrucible-0.4.0 → sqlcrucible-0.4.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlcrucible
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Define a single model that works as both Pydantic and SQLAlchemy, with explicit conversion between the two
5
5
  Project-URL: Homepage, https://sqlcrucible.rdrj.uk
6
6
  Project-URL: Issues, https://github.com/RichardDRJ/sqlcrucible/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.4.0'
22
- __version_tuple__ = version_tuple = (0, 4, 0)
21
+ __version__ = version = '0.4.1'
22
+ __version_tuple__ = version_tuple = (0, 4, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -278,7 +278,11 @@ class SQLCrucibleEntity:
278
278
 
279
279
  This method converts a SQLAlchemy model instance into the corresponding
280
280
  entity class. For polymorphic models, it automatically selects the most
281
- specific entity subclass that matches the model type.
281
+ specific entity subclass that matches the model type — searching the
282
+ whole subclass subtree, not just ``cls``'s direct children, so that a
283
+ polymorphic query through a *generic* STI root reaches the named
284
+ concrete subclasses rather than only the transparent ``Foo[X]``
285
+ parameterisations that share the root's automodel.
282
286
 
283
287
  Args:
284
288
  sa_model: A SQLAlchemy model instance to convert.
@@ -302,15 +306,18 @@ class SQLCrucibleEntity:
302
306
  f"Hint: Make sure you're passing a SQLAlchemy model that was created from "
303
307
  f"this entity class or one of its subclasses."
304
308
  )
305
- best_subclasses = sorted(
306
- cls.__subclasses__(),
307
- key=lambda it: mro_distance(sa_model.__class__, it.__sqlalchemy_type__),
308
- )
309
- if best_subclasses:
310
- best_match = best_subclasses[0]
311
- else:
312
- best_match = cls
309
+ sa_class = sa_model.__class__
310
+
311
+ def subtree(entity: type[Any]) -> Iterator[type[Any]]:
312
+ yield entity
313
+ for subclass in entity.__subclasses__():
314
+ yield from subtree(subclass)
313
315
 
316
+ best_match = min(
317
+ (entity for entity in subtree(cls) if issubclass(sa_class, entity.__sqlalchemy_type__)),
318
+ key=lambda entity: mro_distance(sa_class, entity.__sqlalchemy_type__),
319
+ default=cls,
320
+ )
314
321
  return best_match._from_sa_model(sa_model)
315
322
 
316
323
  @classmethod
@@ -100,6 +100,37 @@ def test_polymorphic_query_through_root_dispatches_to_subclass(engine):
100
100
  assert isinstance(row, SAType[Single])
101
101
 
102
102
 
103
+ def test_from_sa_model_through_generic_root_yields_the_named_subclass(engine):
104
+ """A heterogeneous polymorphic query through the generic STI root, fed
105
+ back through ``Release.from_sa_model`` (not ``Single``/``Album``
106
+ directly), yields the named concrete subclass for each row — with its
107
+ ``details`` re-validated to that subclass's parameterised type. The
108
+ search has to walk past the transparent ``Release[SingleDetails]`` /
109
+ ``Release[AlbumDetails]`` parameterisations (which share the root's
110
+ automodel) to reach ``Single`` / ``Album``."""
111
+ with Session(engine) as session:
112
+ session.add(
113
+ Single(details=SingleDetails(a_side="Heroes", b_side="V-2 Schneider")).to_sa_model()
114
+ )
115
+ session.add(
116
+ Album(
117
+ details=AlbumDetails(track_titles=["Speed of Life", "Breaking Glass"])
118
+ ).to_sa_model()
119
+ )
120
+ session.commit()
121
+
122
+ rows = session.execute(select(SAType[Release])).scalars().all()
123
+
124
+ by_kind = {row.kind: Release.from_sa_model(row) for row in rows}
125
+
126
+ assert isinstance(by_kind["single"], Single)
127
+ assert by_kind["single"].details == SingleDetails(a_side="Heroes", b_side="V-2 Schneider")
128
+ assert isinstance(by_kind["album"], Album)
129
+ assert by_kind["album"].details == AlbumDetails(
130
+ track_titles=["Speed of Life", "Breaking Glass"]
131
+ )
132
+
133
+
103
134
  def test_stub_generation_is_well_formed(tmp_path: Path):
104
135
  """No bracket-named automodel class is created, so the generated
105
136
  stubs contain no class whose name has ``[`` ``]``; the parameterised
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes