async-orm-kit 1.0.0__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 (228) hide show
  1. async_orm_kit-1.0.0/.editorconfig +11 -0
  2. async_orm_kit-1.0.0/.gitattributes +5 -0
  3. async_orm_kit-1.0.0/.github/dependabot.yml +11 -0
  4. async_orm_kit-1.0.0/.github/scripts/check_pytest_junit.py +52 -0
  5. async_orm_kit-1.0.0/.github/scripts/resolve_release.py +58 -0
  6. async_orm_kit-1.0.0/.github/scripts/validate_distribution.py +394 -0
  7. async_orm_kit-1.0.0/.github/workflows/ci.yml +256 -0
  8. async_orm_kit-1.0.0/.github/workflows/release.yml +109 -0
  9. async_orm_kit-1.0.0/.gitignore +42 -0
  10. async_orm_kit-1.0.0/.pre-commit-config.yaml +47 -0
  11. async_orm_kit-1.0.0/.python-version +1 -0
  12. async_orm_kit-1.0.0/.readthedocs.yaml +14 -0
  13. async_orm_kit-1.0.0/AUTHORS.md +9 -0
  14. async_orm_kit-1.0.0/CHANGELOG.md +15 -0
  15. async_orm_kit-1.0.0/CODE_OF_CONDUCT.md +347 -0
  16. async_orm_kit-1.0.0/CONTRIBUTING.md +419 -0
  17. async_orm_kit-1.0.0/CONTRIBUTORS.md +16 -0
  18. async_orm_kit-1.0.0/LICENSE +21 -0
  19. async_orm_kit-1.0.0/PKG-INFO +318 -0
  20. async_orm_kit-1.0.0/README.md +260 -0
  21. async_orm_kit-1.0.0/SECURITY.md +15 -0
  22. async_orm_kit-1.0.0/docs/advanced/aggregation.rst +93 -0
  23. async_orm_kit-1.0.0/docs/advanced/expressions.rst +109 -0
  24. async_orm_kit-1.0.0/docs/advanced/extension.rst +81 -0
  25. async_orm_kit-1.0.0/docs/advanced/json-array.rst +89 -0
  26. async_orm_kit-1.0.0/docs/advanced/locking.rst +29 -0
  27. async_orm_kit-1.0.0/docs/advanced/observability.rst +49 -0
  28. async_orm_kit-1.0.0/docs/advanced/pagination.rst +49 -0
  29. async_orm_kit-1.0.0/docs/advanced/sets-cte.rst +82 -0
  30. async_orm_kit-1.0.0/docs/advanced/subqueries.rst +64 -0
  31. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-001.md +76 -0
  32. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-002.md +210 -0
  33. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-003.md +43 -0
  34. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-004.md +335 -0
  35. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-005.md +345 -0
  36. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-006.md +126 -0
  37. async_orm_kit-1.0.0/docs/architecture/ADR/ADR-007.md +173 -0
  38. async_orm_kit-1.0.0/docs/architecture/ADR/README.md +13 -0
  39. async_orm_kit-1.0.0/docs/architecture/developer_guide.md +1687 -0
  40. async_orm_kit-1.0.0/docs/architecture/feature_list.md +132 -0
  41. async_orm_kit-1.0.0/docs/architecture/migration_guide.md +715 -0
  42. async_orm_kit-1.0.0/docs/compatibility.rst +52 -0
  43. async_orm_kit-1.0.0/docs/conf.py +45 -0
  44. async_orm_kit-1.0.0/docs/guide/managers.rst +64 -0
  45. async_orm_kit-1.0.0/docs/guide/models.rst +70 -0
  46. async_orm_kit-1.0.0/docs/guide/observers.rst +62 -0
  47. async_orm_kit-1.0.0/docs/guide/performance.rst +55 -0
  48. async_orm_kit-1.0.0/docs/guide/querysets.rst +113 -0
  49. async_orm_kit-1.0.0/docs/guide/relationships.rst +71 -0
  50. async_orm_kit-1.0.0/docs/guide/security.rst +48 -0
  51. async_orm_kit-1.0.0/docs/guide/sessions.rst +79 -0
  52. async_orm_kit-1.0.0/docs/guide/troubleshooting.rst +66 -0
  53. async_orm_kit-1.0.0/docs/guide/validation.rst +60 -0
  54. async_orm_kit-1.0.0/docs/index.md +22 -0
  55. async_orm_kit-1.0.0/docs/index.rst +140 -0
  56. async_orm_kit-1.0.0/docs/installation.rst +96 -0
  57. async_orm_kit-1.0.0/docs/integrations/advanced-expressions.md +16 -0
  58. async_orm_kit-1.0.0/docs/integrations/advanced-queries.md +13 -0
  59. async_orm_kit-1.0.0/docs/integrations/advanced-sql.md +169 -0
  60. async_orm_kit-1.0.0/docs/integrations/alembic.md +120 -0
  61. async_orm_kit-1.0.0/docs/integrations/async-session.md +191 -0
  62. async_orm_kit-1.0.0/docs/integrations/extensibility.md +146 -0
  63. async_orm_kit-1.0.0/docs/integrations/fastapi.md +108 -0
  64. async_orm_kit-1.0.0/docs/integrations/mysql.md +120 -0
  65. async_orm_kit-1.0.0/docs/integrations/observability.md +146 -0
  66. async_orm_kit-1.0.0/docs/integrations/postgresql.md +76 -0
  67. async_orm_kit-1.0.0/docs/integrations/storage.md +164 -0
  68. async_orm_kit-1.0.0/docs/integrations-rst/alembic.rst +66 -0
  69. async_orm_kit-1.0.0/docs/integrations-rst/fastapi.rst +86 -0
  70. async_orm_kit-1.0.0/docs/integrations-rst/mysql.rst +60 -0
  71. async_orm_kit-1.0.0/docs/integrations-rst/postgresql.rst +54 -0
  72. async_orm_kit-1.0.0/docs/integrations-rst/sqlite.rst +31 -0
  73. async_orm_kit-1.0.0/docs/integrations-rst/storage.rst +71 -0
  74. async_orm_kit-1.0.0/docs/introduction.rst +57 -0
  75. async_orm_kit-1.0.0/docs/maintainers/architecture.rst +37 -0
  76. async_orm_kit-1.0.0/docs/maintainers/development.rst +75 -0
  77. async_orm_kit-1.0.0/docs/maintainers/release.rst +28 -0
  78. async_orm_kit-1.0.0/docs/quickstart.rst +103 -0
  79. async_orm_kit-1.0.0/docs/reference/errors.rst +40 -0
  80. async_orm_kit-1.0.0/docs/reference/expressions.rst +147 -0
  81. async_orm_kit-1.0.0/docs/reference/fields.rst +20 -0
  82. async_orm_kit-1.0.0/docs/reference/index.rst +22 -0
  83. async_orm_kit-1.0.0/docs/reference/integrations.rst +40 -0
  84. async_orm_kit-1.0.0/docs/reference/manager.rst +18 -0
  85. async_orm_kit-1.0.0/docs/reference/model.rst +23 -0
  86. async_orm_kit-1.0.0/docs/reference/queryset.rst +24 -0
  87. async_orm_kit-1.0.0/pyproject.toml +271 -0
  88. async_orm_kit-1.0.0/pyproject.toml.orig +212 -0
  89. async_orm_kit-1.0.0/src/async_orm_kit/__init__.py +151 -0
  90. async_orm_kit-1.0.0/src/async_orm_kit/_logging.py +58 -0
  91. async_orm_kit-1.0.0/src/async_orm_kit/_version.py +1 -0
  92. async_orm_kit-1.0.0/src/async_orm_kit/config.py +31 -0
  93. async_orm_kit-1.0.0/src/async_orm_kit/exceptions.py +353 -0
  94. async_orm_kit-1.0.0/src/async_orm_kit/integrations/__init__.py +15 -0
  95. async_orm_kit-1.0.0/src/async_orm_kit/integrations/alembic.py +56 -0
  96. async_orm_kit-1.0.0/src/async_orm_kit/integrations/fastapi.py +134 -0
  97. async_orm_kit-1.0.0/src/async_orm_kit/integrations/storage.py +213 -0
  98. async_orm_kit-1.0.0/src/async_orm_kit/manager/__init__.py +3 -0
  99. async_orm_kit-1.0.0/src/async_orm_kit/manager/manager.py +568 -0
  100. async_orm_kit-1.0.0/src/async_orm_kit/models/__init__.py +3 -0
  101. async_orm_kit-1.0.0/src/async_orm_kit/models/base.py +568 -0
  102. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/__init__.py +23 -0
  103. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/exceptions.py +10 -0
  104. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/__init__.py +11 -0
  105. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/base.py +155 -0
  106. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/file.py +697 -0
  107. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/string.py +38 -0
  108. async_orm_kit-1.0.0/src/async_orm_kit/models/fields/validators.py +110 -0
  109. async_orm_kit-1.0.0/src/async_orm_kit/models/meta.py +181 -0
  110. async_orm_kit-1.0.0/src/async_orm_kit/models/mixins.py +19 -0
  111. async_orm_kit-1.0.0/src/async_orm_kit/observers/__init__.py +4 -0
  112. async_orm_kit-1.0.0/src/async_orm_kit/observers/mixin.py +26 -0
  113. async_orm_kit-1.0.0/src/async_orm_kit/observers/model_observer.py +20 -0
  114. async_orm_kit-1.0.0/src/async_orm_kit/observers/queryset_observer.py +38 -0
  115. async_orm_kit-1.0.0/src/async_orm_kit/py.typed +0 -0
  116. async_orm_kit-1.0.0/src/async_orm_kit/query/__init__.py +0 -0
  117. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/__init__.py +8 -0
  118. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/avg.py +71 -0
  119. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/base.py +317 -0
  120. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/count.py +115 -0
  121. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/max.py +76 -0
  122. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/min.py +76 -0
  123. async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/sum.py +73 -0
  124. async_orm_kit-1.0.0/src/async_orm_kit/query/compiler.py +574 -0
  125. async_orm_kit-1.0.0/src/async_orm_kit/query/debugger.py +269 -0
  126. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/__init__.py +62 -0
  127. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/analysis.py +56 -0
  128. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/case.py +321 -0
  129. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/collections.py +251 -0
  130. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/f.py +362 -0
  131. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/functions.py +202 -0
  132. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/subquery.py +206 -0
  133. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/value.py +101 -0
  134. async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/window.py +95 -0
  135. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/__init__.py +121 -0
  136. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/conditions.py +315 -0
  137. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/correlated.py +86 -0
  138. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/json_paths.py +167 -0
  139. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/parser.py +276 -0
  140. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/__init__.py +5 -0
  141. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/context.py +150 -0
  142. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/resolver.py +572 -0
  143. async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/types.py +150 -0
  144. async_orm_kit-1.0.0/src/async_orm_kit/query/mixins/__init__.py +0 -0
  145. async_orm_kit-1.0.0/src/async_orm_kit/query/mixins/combinable.py +376 -0
  146. async_orm_kit-1.0.0/src/async_orm_kit/query/mixins/orderable.py +208 -0
  147. async_orm_kit-1.0.0/src/async_orm_kit/query/observability.py +346 -0
  148. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/__init__.py +0 -0
  149. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/directives.py +117 -0
  150. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/factory.py +40 -0
  151. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/__init__.py +15 -0
  152. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/base.py +47 -0
  153. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/cartesian_detection.py +74 -0
  154. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/depth_splitting.py +78 -0
  155. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/join_promotion.py +81 -0
  156. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/path_conflicts.py +103 -0
  157. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/redundant_path.py +67 -0
  158. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/strategy_selection.py +49 -0
  159. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/planner.py +160 -0
  160. async_orm_kit-1.0.0/src/async_orm_kit/query/planning/protocols.py +78 -0
  161. async_orm_kit-1.0.0/src/async_orm_kit/query/postgres/__init__.py +0 -0
  162. async_orm_kit-1.0.0/src/async_orm_kit/query/postgres/optimizer.py +189 -0
  163. async_orm_kit-1.0.0/src/async_orm_kit/query/q.py +596 -0
  164. async_orm_kit-1.0.0/src/async_orm_kit/query/relations/__init__.py +0 -0
  165. async_orm_kit-1.0.0/src/async_orm_kit/query/relations/enums.py +16 -0
  166. async_orm_kit-1.0.0/src/async_orm_kit/query/relations/graph.py +908 -0
  167. async_orm_kit-1.0.0/src/async_orm_kit/query/relations/node.py +421 -0
  168. async_orm_kit-1.0.0/src/async_orm_kit/queryset/__init__.py +4 -0
  169. async_orm_kit-1.0.0/src/async_orm_kit/queryset/builder.py +1259 -0
  170. async_orm_kit-1.0.0/src/async_orm_kit/queryset/compound.py +379 -0
  171. async_orm_kit-1.0.0/src/async_orm_kit/queryset/executor.py +718 -0
  172. async_orm_kit-1.0.0/src/async_orm_kit/queryset/queryset.py +1726 -0
  173. async_orm_kit-1.0.0/src/async_orm_kit/queryset/state.py +164 -0
  174. async_orm_kit-1.0.0/src/async_orm_kit/queryset/validators.py +361 -0
  175. async_orm_kit-1.0.0/src/async_orm_kit/queryset/writer.py +755 -0
  176. async_orm_kit-1.0.0/src/async_orm_kit/types.py +23 -0
  177. async_orm_kit-1.0.0/tests/integration/__init__.py +5 -0
  178. async_orm_kit-1.0.0/tests/integration/test_advanced_correlation_async.py +146 -0
  179. async_orm_kit-1.0.0/tests/integration/test_advanced_mysql_async.py +129 -0
  180. async_orm_kit-1.0.0/tests/integration/test_advanced_postgres_async.py +190 -0
  181. async_orm_kit-1.0.0/tests/integration/test_advanced_sql_async.py +152 -0
  182. async_orm_kit-1.0.0/tests/integration/test_advanced_stage7_async.py +133 -0
  183. async_orm_kit-1.0.0/tests/integration/test_alembic.py +83 -0
  184. async_orm_kit-1.0.0/tests/integration/test_async_queryset.py +535 -0
  185. async_orm_kit-1.0.0/tests/integration/test_async_transactions.py +122 -0
  186. async_orm_kit-1.0.0/tests/integration/test_fastapi.py +212 -0
  187. async_orm_kit-1.0.0/tests/integration/test_mapped_columns_async.py +52 -0
  188. async_orm_kit-1.0.0/tests/integration/test_mysql_async.py +519 -0
  189. async_orm_kit-1.0.0/tests/integration/test_observability_async.py +126 -0
  190. async_orm_kit-1.0.0/tests/integration/test_postgres_async.py +726 -0
  191. async_orm_kit-1.0.0/tests/integration/test_provider_acceptance.py +235 -0
  192. async_orm_kit-1.0.0/tests/integration/test_release_regressions.py +200 -0
  193. async_orm_kit-1.0.0/tests/integration/test_stage4_async.py +99 -0
  194. async_orm_kit-1.0.0/tests/integration/test_storage_adapter.py +177 -0
  195. async_orm_kit-1.0.0/tests/test_advanced_correlation.py +330 -0
  196. async_orm_kit-1.0.0/tests/test_advanced_expression_correctness.py +585 -0
  197. async_orm_kit-1.0.0/tests/test_advanced_sql.py +432 -0
  198. async_orm_kit-1.0.0/tests/test_advanced_stage6_dialect_contract.py +169 -0
  199. async_orm_kit-1.0.0/tests/test_advanced_stage7.py +295 -0
  200. async_orm_kit-1.0.0/tests/test_compound_contracts.py +116 -0
  201. async_orm_kit-1.0.0/tests/test_diagnostic_contracts.py +111 -0
  202. async_orm_kit-1.0.0/tests/test_documentation_source.py +108 -0
  203. async_orm_kit-1.0.0/tests/test_executable_examples.py +82 -0
  204. async_orm_kit-1.0.0/tests/test_execution_contracts.py +254 -0
  205. async_orm_kit-1.0.0/tests/test_expression_algebra_contracts.py +197 -0
  206. async_orm_kit-1.0.0/tests/test_extensibility_pagination.py +300 -0
  207. async_orm_kit-1.0.0/tests/test_file_lifecycle_contracts.py +273 -0
  208. async_orm_kit-1.0.0/tests/test_foundation.py +184 -0
  209. async_orm_kit-1.0.0/tests/test_json_postgres_contracts.py +116 -0
  210. async_orm_kit-1.0.0/tests/test_manager_contracts.py +111 -0
  211. async_orm_kit-1.0.0/tests/test_model_lifecycle_contracts.py +191 -0
  212. async_orm_kit-1.0.0/tests/test_model_manager_transactions.py +716 -0
  213. async_orm_kit-1.0.0/tests/test_mutation_contracts.py +110 -0
  214. async_orm_kit-1.0.0/tests/test_mysql_contract.py +210 -0
  215. async_orm_kit-1.0.0/tests/test_planning_contracts.py +332 -0
  216. async_orm_kit-1.0.0/tests/test_query_construction_contracts.py +166 -0
  217. async_orm_kit-1.0.0/tests/test_query_observability.py +347 -0
  218. async_orm_kit-1.0.0/tests/test_queryset_expressions.py +722 -0
  219. async_orm_kit-1.0.0/tests/test_relationship_planning.py +662 -0
  220. async_orm_kit-1.0.0/tests/test_release_edge_contracts.py +304 -0
  221. async_orm_kit-1.0.0/tests/test_release_tools.py +50 -0
  222. async_orm_kit-1.0.0/tests/test_repository_configuration.py +108 -0
  223. async_orm_kit-1.0.0/tests/test_resolution_contracts.py +133 -0
  224. async_orm_kit-1.0.0/tests/test_scalar_contracts.py +208 -0
  225. async_orm_kit-1.0.0/tests/test_storage_adapter.py +482 -0
  226. async_orm_kit-1.0.0/tests/test_stream_completion_contracts.py +112 -0
  227. async_orm_kit-1.0.0/tox.ini +68 -0
  228. async_orm_kit-1.0.0/uv.lock +3162 -0
@@ -0,0 +1,11 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ indent_style = space
8
+ indent_size = 4
9
+
10
+ [*.md]
11
+ trim_trailing_whitespace = false
@@ -0,0 +1,5 @@
1
+ *.py text eol=lf
2
+ *.md text eol=lf
3
+ *.toml text eol=lf
4
+ *.yml text eol=lf
5
+ *.yaml text eol=lf
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: uv
8
+ directory: "/"
9
+ schedule:
10
+ interval: weekly
11
+ open-pull-requests-limit: 5
@@ -0,0 +1,52 @@
1
+ """Validate real execution evidence in pytest JUnit output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import xml.etree.ElementTree as ET
7
+ from pathlib import Path
8
+
9
+
10
+ def validate_report(file: Path, *, min_tests: int = 1) -> int:
11
+ if min_tests < 1:
12
+ raise ValueError("min_tests must be positive")
13
+ root = ET.parse(file).getroot()
14
+ if root.tag not in {"testsuites", "testsuite"}:
15
+ raise ValueError("Expected a pytest JUnit testsuite(s) document")
16
+ cases = list(root.iter("testcase"))
17
+ skipped = sum(case.find("skipped") is not None for case in cases)
18
+ failures = sum(
19
+ case.find("failure") is not None or case.find("error") is not None
20
+ for case in cases
21
+ )
22
+ # Count leaf suites once, even when a producer nests suites.
23
+ suites = [
24
+ suite
25
+ for suite in root.iter("testsuite")
26
+ if not list(suite.iterfind("testsuite"))
27
+ ]
28
+ declared = sum(int(suite.get("tests", "0")) for suite in suites)
29
+ reported_bad = sum(
30
+ int(suite.get(key, "0"))
31
+ for suite in suites
32
+ for key in ("skipped", "failures", "errors")
33
+ )
34
+ tests = len(cases)
35
+ if tests < min_tests or declared != tests or skipped or failures or reported_bad:
36
+ raise ValueError(
37
+ f"Required live tests not fully passed: {tests=} {declared=} {skipped=} {failures=}"
38
+ )
39
+ return tests
40
+
41
+
42
+ def main():
43
+ parser = argparse.ArgumentParser(description=__doc__)
44
+ parser.add_argument("file", type=Path)
45
+ parser.add_argument("--min-tests", type=int, default=1)
46
+ args = parser.parse_args()
47
+ tests = validate_report(args.file, min_tests=args.min_tests)
48
+ print(f"Verified {tests} actual test cases with no skips/failures")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,58 @@
1
+ """Resolve and validate an immutable stable-release tag without executing tag code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import tomllib
10
+ from pathlib import Path
11
+
12
+
13
+ def git(*args: str) -> str:
14
+ return subprocess.check_output(["git", *args], text=True).strip()
15
+
16
+
17
+ def resolve(tag: str, *, push_sha: str = "") -> str:
18
+ if not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", tag):
19
+ raise RuntimeError("Release tags must have the stable form vMAJOR.MINOR.PATCH")
20
+ sha = git("rev-parse", "--verify", f"refs/tags/{tag}^{{commit}}")
21
+ if not re.fullmatch(r"[a-f0-9]{40}", sha):
22
+ raise RuntimeError("Could not resolve the release tag to a full commit SHA")
23
+ if push_sha and sha != push_sha:
24
+ raise RuntimeError("Release tag moved or does not match the push event commit")
25
+ subprocess.run(
26
+ ["git", "merge-base", "--is-ancestor", sha, "origin/main"], check=True
27
+ )
28
+ project = tomllib.loads(git("show", f"{sha}:pyproject.toml"))["project"]
29
+ if tag != f"v{project['version']}":
30
+ raise RuntimeError("Release tag disagrees with package project.version")
31
+ version_file = git("show", f"{sha}:src/async_orm_kit/_version.py")
32
+ if not re.search(
33
+ rf'^__version__\s*=\s*["\']{re.escape(project["version"])}["\']$',
34
+ version_file,
35
+ re.M,
36
+ ):
37
+ raise RuntimeError("Release tag disagrees with runtime __version__")
38
+ return sha
39
+
40
+
41
+ def main():
42
+ parser = argparse.ArgumentParser(description=__doc__)
43
+ parser.add_argument("--tag", required=True)
44
+ parser.add_argument("--push-sha", default="")
45
+ parser.add_argument("--verify-commit", default="")
46
+ args = parser.parse_args()
47
+ sha = resolve(args.tag, push_sha=args.push_sha)
48
+ if args.verify_commit and sha != args.verify_commit:
49
+ raise RuntimeError("Release tag has changed since candidate resolution")
50
+ output = os.environ.get("GITHUB_OUTPUT")
51
+ if output:
52
+ with Path(output).open("a", encoding="utf-8") as stream:
53
+ stream.write(f"sha={sha}\ntag={args.tag}\n")
54
+ print(f"Verified release tag {args.tag} at immutable SHA {sha}")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,394 @@
1
+ """Validate Async ORM Kit wheel/sdist metadata, contents and isolated installs.
2
+
3
+ Invoke after building exactly one wheel and one sdist. The `--release` flag
4
+ adds fail-closed legal/lockfile/version checks; it must be used for publishing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import hashlib
11
+ import os
12
+ import re
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import tarfile
17
+ import tempfile
18
+ import tomllib
19
+ import zipfile
20
+ from email.parser import BytesParser
21
+ from pathlib import Path, PurePosixPath
22
+
23
+ from packaging.specifiers import SpecifierSet
24
+ from packaging.version import Version
25
+
26
+ ROOT = Path(__file__).resolve().parents[2]
27
+ PACKAGE = "async_orm_kit"
28
+ DIST_NAME = "async-orm-kit"
29
+ FORBIDDEN = {
30
+ "_build",
31
+ ".validation",
32
+ ".release-venv",
33
+ ".git",
34
+ ".venv",
35
+ ".tox",
36
+ ".pytest_cache",
37
+ ".mypy_cache",
38
+ ".ruff_cache",
39
+ "__pycache__",
40
+ "dist",
41
+ "build",
42
+ "venv",
43
+ }
44
+ REQUIRED_SOURCE = {
45
+ "pyproject.toml",
46
+ "README.md",
47
+ "LICENSE",
48
+ "CHANGELOG.md",
49
+ "SECURITY.md",
50
+ "CODE_OF_CONDUCT.md",
51
+ "CONTRIBUTING.md",
52
+ "CONTRIBUTORS.md",
53
+ "AUTHORS.md",
54
+ "tox.ini",
55
+ ".readthedocs.yaml",
56
+ "docs/index.rst",
57
+ "docs/conf.py",
58
+ ".pre-commit-config.yaml",
59
+ ".github/dependabot.yml",
60
+ }
61
+
62
+
63
+ def _safe_path(name: str) -> PurePosixPath:
64
+ path = PurePosixPath(name)
65
+ if (
66
+ path.is_absolute()
67
+ or not path.parts
68
+ or ".." in path.parts
69
+ or "\\" in name
70
+ or ":" in name
71
+ or set(path.parts) & FORBIDDEN
72
+ or path.name.startswith(".env")
73
+ or path.suffix in {".pyc", ".pyo"}
74
+ ):
75
+ raise RuntimeError(f"Unsafe archive entry: {name}")
76
+ return path
77
+
78
+
79
+ def _metadata(contents: dict[str, bytes], *, sdist: bool = False):
80
+ matches = (
81
+ [k for k in contents if k == "PKG-INFO"]
82
+ if sdist
83
+ else [k for k in contents if k.endswith(".dist-info/METADATA")]
84
+ )
85
+ if len(matches) != 1:
86
+ raise RuntimeError("Expected exactly one distribution metadata record")
87
+ return BytesParser().parsebytes(contents[matches[0]])
88
+
89
+
90
+ def _check_metadata(meta, project: dict, release: bool):
91
+ if meta.get("Name", "").lower().replace("_", "-") != DIST_NAME:
92
+ raise RuntimeError("Distribution name does not match async-orm-kit")
93
+ for key, expected in (
94
+ ("Version", project["version"]),
95
+ ("Requires-Python", project["requires-python"]),
96
+ ):
97
+ actual = meta.get(key)
98
+ if key == "Requires-Python":
99
+ matches = actual is not None and SpecifierSet(actual) == SpecifierSet(
100
+ expected
101
+ )
102
+ else:
103
+ matches = actual == expected
104
+ if not matches:
105
+ raise RuntimeError(f"{key} differs from pyproject.toml")
106
+ if (
107
+ meta.get("Description-Content-Type", "").split(";")[0].strip()
108
+ != "text/markdown"
109
+ ):
110
+ raise RuntimeError("Missing Markdown description content type")
111
+ if (
112
+ "Programming Language :: Python :: Implementation :: CPython"
113
+ not in meta.get_all("Classifier", [])
114
+ ):
115
+ raise RuntimeError("CPython classifier missing")
116
+
117
+ installed_extras = {
118
+ value.strip().lower().replace("_", "-")
119
+ for value in meta.get_all("Provides-Extra", [])
120
+ }
121
+ configured_extras = set(project["optional-dependencies"])
122
+ if installed_extras != configured_extras:
123
+ raise RuntimeError("Published extras differ from pyproject.toml")
124
+ mandatory = []
125
+ for requirement in meta.get_all("Requires-Dist", []):
126
+ if not requirement.partition(";")[2].strip():
127
+ mandatory.append(requirement)
128
+ if len(mandatory) != 1 or not re.match(
129
+ r"(?i)^sqlalchemy(?:\[asyncio\])?\s*[<>=]", mandatory[0]
130
+ ):
131
+ raise RuntimeError(f"Expected mandatory SQLAlchemy only, found {mandatory!r}")
132
+ if "async-storage-kit" in "\n".join(mandatory).lower():
133
+ raise RuntimeError("Storage dependency leaked into mandatory requirements")
134
+ if meta.get("License-Expression") != project.get("license"):
135
+ raise RuntimeError("License expression differs from pyproject.toml")
136
+ if "LICENSE" not in meta.get_all("License-File", []):
137
+ raise RuntimeError("LICENSE is missing from distribution metadata")
138
+ if release:
139
+ if Version(project["version"]).is_prerelease:
140
+ raise RuntimeError(
141
+ "Do not publish a development version via the stable release workflow"
142
+ )
143
+ if not (ROOT / "LICENSE").is_file():
144
+ raise RuntimeError(
145
+ "Maintainer-approved LICENSE is required for public release"
146
+ )
147
+ if not (ROOT / "uv.lock").is_file():
148
+ raise RuntimeError(
149
+ "An approved and reproducible uv.lock is required for release"
150
+ )
151
+ if "LICENSE" not in meta.get_all("License-File", []):
152
+ raise RuntimeError("LICENSE is not included in distribution metadata")
153
+ if not project.get("urls"):
154
+ raise RuntimeError(
155
+ "Verified project URLs must be configured before release"
156
+ )
157
+
158
+
159
+ def _source_required() -> set[str]:
160
+ required = set(REQUIRED_SOURCE)
161
+ for directory, suffixes in (
162
+ ("src", {".py", ".pyi", ".typed"}),
163
+ ("tests", {".py"}),
164
+ ("docs", {".md", ".rst", ".py"}),
165
+ (".github/scripts", {".py"}),
166
+ (".github/workflows", {".yml", ".yaml"}),
167
+ ):
168
+ base = ROOT / directory
169
+ if not base.exists():
170
+ continue
171
+ for path in base.rglob("*"):
172
+ rel = path.relative_to(ROOT)
173
+ if (
174
+ path.is_file()
175
+ and not (set(rel.parts) & FORBIDDEN)
176
+ and path.suffix in suffixes
177
+ ):
178
+ required.add(rel.as_posix())
179
+ if (ROOT / "uv.lock").is_file():
180
+ required.add("uv.lock")
181
+ if (ROOT / "LICENSE").is_file():
182
+ required.add("LICENSE")
183
+ return required
184
+
185
+
186
+ def _read_wheel(path: Path) -> dict[str, bytes]:
187
+ with zipfile.ZipFile(path) as zip_file:
188
+ names = zip_file.namelist()
189
+ if len(names) != len(set(names)):
190
+ raise RuntimeError("Duplicate wheel file")
191
+ for name in names:
192
+ _safe_path(name)
193
+ data = {
194
+ member.filename: zip_file.read(member)
195
+ for member in zip_file.infolist()
196
+ if not member.is_dir()
197
+ }
198
+ licenses = [
199
+ value
200
+ for name, value in data.items()
201
+ if name.endswith(".dist-info/licenses/LICENSE")
202
+ ]
203
+ if licenses != [(ROOT / "LICENSE").read_bytes()]:
204
+ raise RuntimeError("Wheel must include the approved LICENSE text")
205
+ if f"{PACKAGE}/py.typed" not in data:
206
+ raise RuntimeError("Wheel missing py.typed marker")
207
+ source_files = {
208
+ path.relative_to(ROOT / "src").as_posix(): path.read_bytes()
209
+ for path in (ROOT / "src" / PACKAGE).rglob("*")
210
+ if path.is_file() and path.suffix in {".py", ".pyi", ".typed"}
211
+ }
212
+ for filename, contents in source_files.items():
213
+ if data.get(filename) != contents:
214
+ raise RuntimeError(f"Wheel runtime file missing or different: {filename}")
215
+ for filename in data:
216
+ if filename.startswith(f"{PACKAGE}/") or ".dist-info/" in filename:
217
+ continue
218
+ raise RuntimeError(f"Unexpected wheel file: {filename}")
219
+ return data
220
+
221
+
222
+ def _read_sdist(path: Path) -> dict[str, bytes]:
223
+ with tarfile.open(path) as tar:
224
+ members = tar.getmembers()
225
+ roots = set()
226
+ data = {}
227
+ for member in members:
228
+ member_path = _safe_path(member.name)
229
+ roots.add(member_path.parts[0])
230
+ if member.isdir():
231
+ continue
232
+ if not member.isfile() or len(member_path.parts) < 2:
233
+ raise RuntimeError(f"Unsafe sdist entry type: {member.name}")
234
+ rel = PurePosixPath(*member_path.parts[1:]).as_posix()
235
+ if rel in data:
236
+ raise RuntimeError(f"Duplicate sdist path: {rel}")
237
+ file_obj = tar.extractfile(member)
238
+ if file_obj is None:
239
+ raise RuntimeError(f"Unreadable sdist entry: {rel}")
240
+ with file_obj:
241
+ data[rel] = file_obj.read()
242
+ if len(roots) != 1:
243
+ raise RuntimeError("Source archive must have exactly one root")
244
+ missing = (_source_required() | {"PKG-INFO"}) - data.keys()
245
+ if missing:
246
+ raise RuntimeError(f"Source archive missing files: {sorted(missing)}")
247
+ for rel in _source_required():
248
+ if rel == "pyproject.toml":
249
+ if tomllib.loads(data[rel].decode()) != tomllib.loads(
250
+ (ROOT / rel).read_text()
251
+ ):
252
+ raise RuntimeError("Source archive project metadata differs")
253
+ elif data[rel] != (ROOT / rel).read_bytes():
254
+ raise RuntimeError(f"Source archive file differs: {rel}")
255
+ return data
256
+
257
+
258
+ def _run(
259
+ command: list[str],
260
+ *,
261
+ cwd: Path | None = None,
262
+ env: dict | None = None,
263
+ timeout: int = 240,
264
+ ):
265
+ subprocess.run(command, cwd=cwd, env=env, check=True, timeout=timeout)
266
+
267
+
268
+ def _test_source(files: dict[str, bytes], wheel_files: dict[str, bytes], uv: str):
269
+ with tempfile.TemporaryDirectory(prefix="async-orm-kit-sdist-") as temp:
270
+ root = Path(temp)
271
+ source = root / "source"
272
+ for rel, content in files.items():
273
+ file = source.joinpath(*PurePosixPath(rel).parts)
274
+ file.parent.mkdir(parents=True, exist_ok=True)
275
+ file.write_bytes(content)
276
+ env = dict(
277
+ os.environ, PYTHONPATH=str(source / "src"), PYTHONDONTWRITEBYTECODE="1"
278
+ )
279
+ _run(
280
+ [sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider", "tests"],
281
+ cwd=source,
282
+ env=env,
283
+ timeout=600,
284
+ )
285
+ rebuilt = root / "rebuilt"
286
+ _run(
287
+ [
288
+ uv,
289
+ "build",
290
+ "--no-sources",
291
+ "--wheel",
292
+ "--python",
293
+ sys.executable,
294
+ "--out-dir",
295
+ str(rebuilt),
296
+ ],
297
+ cwd=source,
298
+ env=env,
299
+ )
300
+ wheels = list(rebuilt.glob("*.whl"))
301
+ if len(wheels) != 1 or _wheel_contents_only(wheels[0]) != wheel_files:
302
+ raise RuntimeError("Rebuilt wheel content differs from validated wheel")
303
+
304
+
305
+ def _wheel_contents_only(path: Path) -> dict[str, bytes]:
306
+ with zipfile.ZipFile(path) as z:
307
+ return {i.filename: z.read(i) for i in z.infolist() if not i.is_dir()}
308
+
309
+
310
+ def _test_install(wheel: Path, uv: str, extras: bool):
311
+ candidates = [None]
312
+ if extras:
313
+ candidates += list(
314
+ tomllib.loads((ROOT / "pyproject.toml").read_text())["project"][
315
+ "optional-dependencies"
316
+ ]
317
+ )
318
+ with tempfile.TemporaryDirectory(prefix="async-orm-kit-install-") as temp:
319
+ for extra in candidates:
320
+ root = Path(temp) / (extra or "core")
321
+ venv = root / "venv"
322
+ _run([uv, "venv", "--python", sys.executable, str(venv)])
323
+ py = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
324
+ requirement = str(wheel) + (f"[{extra}]" if extra else "")
325
+ _run(
326
+ [
327
+ uv,
328
+ "pip",
329
+ "install",
330
+ "--link-mode",
331
+ "copy",
332
+ "--python",
333
+ str(py),
334
+ requirement,
335
+ ]
336
+ )
337
+ _run([uv, "pip", "check", "--python", str(py)])
338
+ code = (
339
+ "from importlib.metadata import version; from async_orm_kit import Model, Manager; "
340
+ "assert Model.metadata is not None; assert Manager is not None; "
341
+ f"assert version('async-orm-kit') == {tomllib.loads((ROOT / 'pyproject.toml').read_text())['project']['version']!r}"
342
+ )
343
+ if extra is None:
344
+ code += (
345
+ "; import sys; assert 'fastapi' not in sys.modules"
346
+ "; assert 'alembic' not in sys.modules"
347
+ "; assert 'async_storage_kit' not in sys.modules"
348
+ )
349
+ modules = {
350
+ "sqlite": "aiosqlite",
351
+ "postgres": "asyncpg",
352
+ "mysql": "asyncmy",
353
+ "storage": "async_storage_kit",
354
+ "fastapi": "fastapi",
355
+ "migrations": "alembic",
356
+ "docs": "sphinx",
357
+ "test": "pytest",
358
+ }
359
+ if extra in modules:
360
+ code += f"; import {modules[extra]}"
361
+ _run([str(py), "-I", "-c", code], cwd=root)
362
+ print("Isolated install passed:", extra or "core")
363
+
364
+
365
+ def main():
366
+ parser = argparse.ArgumentParser(description=__doc__)
367
+ parser.add_argument("directory", nargs="?", type=Path, default=Path("dist"))
368
+ parser.add_argument("--test-sdist", action="store_true")
369
+ parser.add_argument("--test-extras", action="store_true")
370
+ parser.add_argument("--release", action="store_true")
371
+ args = parser.parse_args()
372
+ directory = args.directory.resolve()
373
+ wheels, sdists = list(directory.glob("*.whl")), list(directory.glob("*.tar.gz"))
374
+ if len(wheels) != 1 or len(sdists) != 1:
375
+ raise RuntimeError("Exactly one wheel and one source archive are required")
376
+ with (ROOT / "pyproject.toml").open("rb") as f:
377
+ project = tomllib.load(f)["project"]
378
+ wheel_files = _read_wheel(wheels[0])
379
+ sdist_files = _read_sdist(sdists[0])
380
+ _check_metadata(_metadata(wheel_files), project, args.release)
381
+ _check_metadata(_metadata(sdist_files, sdist=True), project, args.release)
382
+ for path in (wheels[0], sdists[0]):
383
+ print(f"SHA256 {path.name}: {hashlib.sha256(path.read_bytes()).hexdigest()}")
384
+ uv = shutil.which("uv")
385
+ if uv is None:
386
+ raise RuntimeError("uv is required for isolated install validation")
387
+ _test_install(wheels[0], uv, args.test_extras)
388
+ if args.test_sdist:
389
+ _test_source(sdist_files, wheel_files, uv)
390
+ print("Distribution validation complete")
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()