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.
- async_orm_kit-1.0.0/.editorconfig +11 -0
- async_orm_kit-1.0.0/.gitattributes +5 -0
- async_orm_kit-1.0.0/.github/dependabot.yml +11 -0
- async_orm_kit-1.0.0/.github/scripts/check_pytest_junit.py +52 -0
- async_orm_kit-1.0.0/.github/scripts/resolve_release.py +58 -0
- async_orm_kit-1.0.0/.github/scripts/validate_distribution.py +394 -0
- async_orm_kit-1.0.0/.github/workflows/ci.yml +256 -0
- async_orm_kit-1.0.0/.github/workflows/release.yml +109 -0
- async_orm_kit-1.0.0/.gitignore +42 -0
- async_orm_kit-1.0.0/.pre-commit-config.yaml +47 -0
- async_orm_kit-1.0.0/.python-version +1 -0
- async_orm_kit-1.0.0/.readthedocs.yaml +14 -0
- async_orm_kit-1.0.0/AUTHORS.md +9 -0
- async_orm_kit-1.0.0/CHANGELOG.md +15 -0
- async_orm_kit-1.0.0/CODE_OF_CONDUCT.md +347 -0
- async_orm_kit-1.0.0/CONTRIBUTING.md +419 -0
- async_orm_kit-1.0.0/CONTRIBUTORS.md +16 -0
- async_orm_kit-1.0.0/LICENSE +21 -0
- async_orm_kit-1.0.0/PKG-INFO +318 -0
- async_orm_kit-1.0.0/README.md +260 -0
- async_orm_kit-1.0.0/SECURITY.md +15 -0
- async_orm_kit-1.0.0/docs/advanced/aggregation.rst +93 -0
- async_orm_kit-1.0.0/docs/advanced/expressions.rst +109 -0
- async_orm_kit-1.0.0/docs/advanced/extension.rst +81 -0
- async_orm_kit-1.0.0/docs/advanced/json-array.rst +89 -0
- async_orm_kit-1.0.0/docs/advanced/locking.rst +29 -0
- async_orm_kit-1.0.0/docs/advanced/observability.rst +49 -0
- async_orm_kit-1.0.0/docs/advanced/pagination.rst +49 -0
- async_orm_kit-1.0.0/docs/advanced/sets-cte.rst +82 -0
- async_orm_kit-1.0.0/docs/advanced/subqueries.rst +64 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-001.md +76 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-002.md +210 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-003.md +43 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-004.md +335 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-005.md +345 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-006.md +126 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/ADR-007.md +173 -0
- async_orm_kit-1.0.0/docs/architecture/ADR/README.md +13 -0
- async_orm_kit-1.0.0/docs/architecture/developer_guide.md +1687 -0
- async_orm_kit-1.0.0/docs/architecture/feature_list.md +132 -0
- async_orm_kit-1.0.0/docs/architecture/migration_guide.md +715 -0
- async_orm_kit-1.0.0/docs/compatibility.rst +52 -0
- async_orm_kit-1.0.0/docs/conf.py +45 -0
- async_orm_kit-1.0.0/docs/guide/managers.rst +64 -0
- async_orm_kit-1.0.0/docs/guide/models.rst +70 -0
- async_orm_kit-1.0.0/docs/guide/observers.rst +62 -0
- async_orm_kit-1.0.0/docs/guide/performance.rst +55 -0
- async_orm_kit-1.0.0/docs/guide/querysets.rst +113 -0
- async_orm_kit-1.0.0/docs/guide/relationships.rst +71 -0
- async_orm_kit-1.0.0/docs/guide/security.rst +48 -0
- async_orm_kit-1.0.0/docs/guide/sessions.rst +79 -0
- async_orm_kit-1.0.0/docs/guide/troubleshooting.rst +66 -0
- async_orm_kit-1.0.0/docs/guide/validation.rst +60 -0
- async_orm_kit-1.0.0/docs/index.md +22 -0
- async_orm_kit-1.0.0/docs/index.rst +140 -0
- async_orm_kit-1.0.0/docs/installation.rst +96 -0
- async_orm_kit-1.0.0/docs/integrations/advanced-expressions.md +16 -0
- async_orm_kit-1.0.0/docs/integrations/advanced-queries.md +13 -0
- async_orm_kit-1.0.0/docs/integrations/advanced-sql.md +169 -0
- async_orm_kit-1.0.0/docs/integrations/alembic.md +120 -0
- async_orm_kit-1.0.0/docs/integrations/async-session.md +191 -0
- async_orm_kit-1.0.0/docs/integrations/extensibility.md +146 -0
- async_orm_kit-1.0.0/docs/integrations/fastapi.md +108 -0
- async_orm_kit-1.0.0/docs/integrations/mysql.md +120 -0
- async_orm_kit-1.0.0/docs/integrations/observability.md +146 -0
- async_orm_kit-1.0.0/docs/integrations/postgresql.md +76 -0
- async_orm_kit-1.0.0/docs/integrations/storage.md +164 -0
- async_orm_kit-1.0.0/docs/integrations-rst/alembic.rst +66 -0
- async_orm_kit-1.0.0/docs/integrations-rst/fastapi.rst +86 -0
- async_orm_kit-1.0.0/docs/integrations-rst/mysql.rst +60 -0
- async_orm_kit-1.0.0/docs/integrations-rst/postgresql.rst +54 -0
- async_orm_kit-1.0.0/docs/integrations-rst/sqlite.rst +31 -0
- async_orm_kit-1.0.0/docs/integrations-rst/storage.rst +71 -0
- async_orm_kit-1.0.0/docs/introduction.rst +57 -0
- async_orm_kit-1.0.0/docs/maintainers/architecture.rst +37 -0
- async_orm_kit-1.0.0/docs/maintainers/development.rst +75 -0
- async_orm_kit-1.0.0/docs/maintainers/release.rst +28 -0
- async_orm_kit-1.0.0/docs/quickstart.rst +103 -0
- async_orm_kit-1.0.0/docs/reference/errors.rst +40 -0
- async_orm_kit-1.0.0/docs/reference/expressions.rst +147 -0
- async_orm_kit-1.0.0/docs/reference/fields.rst +20 -0
- async_orm_kit-1.0.0/docs/reference/index.rst +22 -0
- async_orm_kit-1.0.0/docs/reference/integrations.rst +40 -0
- async_orm_kit-1.0.0/docs/reference/manager.rst +18 -0
- async_orm_kit-1.0.0/docs/reference/model.rst +23 -0
- async_orm_kit-1.0.0/docs/reference/queryset.rst +24 -0
- async_orm_kit-1.0.0/pyproject.toml +271 -0
- async_orm_kit-1.0.0/pyproject.toml.orig +212 -0
- async_orm_kit-1.0.0/src/async_orm_kit/__init__.py +151 -0
- async_orm_kit-1.0.0/src/async_orm_kit/_logging.py +58 -0
- async_orm_kit-1.0.0/src/async_orm_kit/_version.py +1 -0
- async_orm_kit-1.0.0/src/async_orm_kit/config.py +31 -0
- async_orm_kit-1.0.0/src/async_orm_kit/exceptions.py +353 -0
- async_orm_kit-1.0.0/src/async_orm_kit/integrations/__init__.py +15 -0
- async_orm_kit-1.0.0/src/async_orm_kit/integrations/alembic.py +56 -0
- async_orm_kit-1.0.0/src/async_orm_kit/integrations/fastapi.py +134 -0
- async_orm_kit-1.0.0/src/async_orm_kit/integrations/storage.py +213 -0
- async_orm_kit-1.0.0/src/async_orm_kit/manager/__init__.py +3 -0
- async_orm_kit-1.0.0/src/async_orm_kit/manager/manager.py +568 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/__init__.py +3 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/base.py +568 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/__init__.py +23 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/exceptions.py +10 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/__init__.py +11 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/base.py +155 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/file.py +697 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/typed/string.py +38 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/fields/validators.py +110 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/meta.py +181 -0
- async_orm_kit-1.0.0/src/async_orm_kit/models/mixins.py +19 -0
- async_orm_kit-1.0.0/src/async_orm_kit/observers/__init__.py +4 -0
- async_orm_kit-1.0.0/src/async_orm_kit/observers/mixin.py +26 -0
- async_orm_kit-1.0.0/src/async_orm_kit/observers/model_observer.py +20 -0
- async_orm_kit-1.0.0/src/async_orm_kit/observers/queryset_observer.py +38 -0
- async_orm_kit-1.0.0/src/async_orm_kit/py.typed +0 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/__init__.py +0 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/__init__.py +8 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/avg.py +71 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/base.py +317 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/count.py +115 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/max.py +76 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/min.py +76 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/aggregations/sum.py +73 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/compiler.py +574 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/debugger.py +269 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/__init__.py +62 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/analysis.py +56 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/case.py +321 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/collections.py +251 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/f.py +362 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/functions.py +202 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/subquery.py +206 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/value.py +101 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/expressions/window.py +95 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/__init__.py +121 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/conditions.py +315 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/correlated.py +86 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/json_paths.py +167 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/parser.py +276 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/__init__.py +5 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/context.py +150 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/resolver.py +572 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/lookups/resolver/types.py +150 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/mixins/__init__.py +0 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/mixins/combinable.py +376 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/mixins/orderable.py +208 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/observability.py +346 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/__init__.py +0 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/directives.py +117 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/factory.py +40 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/__init__.py +15 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/base.py +47 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/cartesian_detection.py +74 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/depth_splitting.py +78 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/join_promotion.py +81 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/path_conflicts.py +103 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/redundant_path.py +67 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/passes/strategy_selection.py +49 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/planner.py +160 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/planning/protocols.py +78 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/postgres/__init__.py +0 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/postgres/optimizer.py +189 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/q.py +596 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/relations/__init__.py +0 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/relations/enums.py +16 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/relations/graph.py +908 -0
- async_orm_kit-1.0.0/src/async_orm_kit/query/relations/node.py +421 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/__init__.py +4 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/builder.py +1259 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/compound.py +379 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/executor.py +718 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/queryset.py +1726 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/state.py +164 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/validators.py +361 -0
- async_orm_kit-1.0.0/src/async_orm_kit/queryset/writer.py +755 -0
- async_orm_kit-1.0.0/src/async_orm_kit/types.py +23 -0
- async_orm_kit-1.0.0/tests/integration/__init__.py +5 -0
- async_orm_kit-1.0.0/tests/integration/test_advanced_correlation_async.py +146 -0
- async_orm_kit-1.0.0/tests/integration/test_advanced_mysql_async.py +129 -0
- async_orm_kit-1.0.0/tests/integration/test_advanced_postgres_async.py +190 -0
- async_orm_kit-1.0.0/tests/integration/test_advanced_sql_async.py +152 -0
- async_orm_kit-1.0.0/tests/integration/test_advanced_stage7_async.py +133 -0
- async_orm_kit-1.0.0/tests/integration/test_alembic.py +83 -0
- async_orm_kit-1.0.0/tests/integration/test_async_queryset.py +535 -0
- async_orm_kit-1.0.0/tests/integration/test_async_transactions.py +122 -0
- async_orm_kit-1.0.0/tests/integration/test_fastapi.py +212 -0
- async_orm_kit-1.0.0/tests/integration/test_mapped_columns_async.py +52 -0
- async_orm_kit-1.0.0/tests/integration/test_mysql_async.py +519 -0
- async_orm_kit-1.0.0/tests/integration/test_observability_async.py +126 -0
- async_orm_kit-1.0.0/tests/integration/test_postgres_async.py +726 -0
- async_orm_kit-1.0.0/tests/integration/test_provider_acceptance.py +235 -0
- async_orm_kit-1.0.0/tests/integration/test_release_regressions.py +200 -0
- async_orm_kit-1.0.0/tests/integration/test_stage4_async.py +99 -0
- async_orm_kit-1.0.0/tests/integration/test_storage_adapter.py +177 -0
- async_orm_kit-1.0.0/tests/test_advanced_correlation.py +330 -0
- async_orm_kit-1.0.0/tests/test_advanced_expression_correctness.py +585 -0
- async_orm_kit-1.0.0/tests/test_advanced_sql.py +432 -0
- async_orm_kit-1.0.0/tests/test_advanced_stage6_dialect_contract.py +169 -0
- async_orm_kit-1.0.0/tests/test_advanced_stage7.py +295 -0
- async_orm_kit-1.0.0/tests/test_compound_contracts.py +116 -0
- async_orm_kit-1.0.0/tests/test_diagnostic_contracts.py +111 -0
- async_orm_kit-1.0.0/tests/test_documentation_source.py +108 -0
- async_orm_kit-1.0.0/tests/test_executable_examples.py +82 -0
- async_orm_kit-1.0.0/tests/test_execution_contracts.py +254 -0
- async_orm_kit-1.0.0/tests/test_expression_algebra_contracts.py +197 -0
- async_orm_kit-1.0.0/tests/test_extensibility_pagination.py +300 -0
- async_orm_kit-1.0.0/tests/test_file_lifecycle_contracts.py +273 -0
- async_orm_kit-1.0.0/tests/test_foundation.py +184 -0
- async_orm_kit-1.0.0/tests/test_json_postgres_contracts.py +116 -0
- async_orm_kit-1.0.0/tests/test_manager_contracts.py +111 -0
- async_orm_kit-1.0.0/tests/test_model_lifecycle_contracts.py +191 -0
- async_orm_kit-1.0.0/tests/test_model_manager_transactions.py +716 -0
- async_orm_kit-1.0.0/tests/test_mutation_contracts.py +110 -0
- async_orm_kit-1.0.0/tests/test_mysql_contract.py +210 -0
- async_orm_kit-1.0.0/tests/test_planning_contracts.py +332 -0
- async_orm_kit-1.0.0/tests/test_query_construction_contracts.py +166 -0
- async_orm_kit-1.0.0/tests/test_query_observability.py +347 -0
- async_orm_kit-1.0.0/tests/test_queryset_expressions.py +722 -0
- async_orm_kit-1.0.0/tests/test_relationship_planning.py +662 -0
- async_orm_kit-1.0.0/tests/test_release_edge_contracts.py +304 -0
- async_orm_kit-1.0.0/tests/test_release_tools.py +50 -0
- async_orm_kit-1.0.0/tests/test_repository_configuration.py +108 -0
- async_orm_kit-1.0.0/tests/test_resolution_contracts.py +133 -0
- async_orm_kit-1.0.0/tests/test_scalar_contracts.py +208 -0
- async_orm_kit-1.0.0/tests/test_storage_adapter.py +482 -0
- async_orm_kit-1.0.0/tests/test_stream_completion_contracts.py +112 -0
- async_orm_kit-1.0.0/tox.ini +68 -0
- async_orm_kit-1.0.0/uv.lock +3162 -0
|
@@ -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()
|