django-sqlalchemy-bridge 0.0.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.
@@ -0,0 +1,52 @@
1
+ name: Release
2
+
3
+ # Tag push (v*) -> build -> publish to PyPI via trusted publishing -> GitHub release.
4
+ # PyPI side: pypi.org > Account settings > Publishing > add a pending publisher with
5
+ # owner itda-work, repository django-sqlalchemy-bridge, workflow release.yml, environment pypi.
6
+ # No API token is stored anywhere.
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ # Pinned exactly: setup-uv dropped its floating major tags in v8.
18
+ - uses: astral-sh/setup-uv@v10.0.1
19
+
20
+ - name: Check the version, the CHANGELOG and the classifiers agree
21
+ run: python3 scripts/release_check.py "$GITHUB_REF_NAME"
22
+
23
+ - run: uv build
24
+ - run: uvx twine check dist/*
25
+
26
+ - uses: actions/upload-artifact@v7
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write # PyPI trusted publishing (OIDC)
37
+ contents: write # GitHub release
38
+ steps:
39
+ - uses: actions/download-artifact@v8
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+
44
+ - name: Publish to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
46
+
47
+ - name: GitHub release
48
+ uses: softprops/action-gh-release@v3
49
+ with:
50
+ files: dist/*
51
+ generate_release_notes: true
52
+ prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ *.sqlite3
@@ -0,0 +1,4 @@
1
+ # django-sqlalchemy-bridge AI Guide
2
+
3
+ 정본은 [`CLAUDE.md`](CLAUDE.md)다. 이 파일은 `AGENTS.md`를 찾는 도구를 위한 포인터일 뿐이고,
4
+ 같은 지침을 두 곳에 두지 않는다 — 두면 어긋난다.
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.0.1] - 2026-10-25
8
+
9
+ ### Added
10
+
11
+ - Name reservation and design README. **Experimental** project; no public API.
@@ -0,0 +1,21 @@
1
+ # django-sqlalchemy-bridge AI Guide
2
+
3
+ > Django 연결·트랜잭션 안에서 SQLAlchemy 2.x Core 표현식을 쓰게 하는 라이브러리. **설계 단계(0.0.x)** — 공개 API 없음.
4
+
5
+ ## 정본
6
+
7
+ | 무엇 | 정본 |
8
+ |------|------|
9
+ | 의존성, 지원 Python | `pyproject.toml` |
10
+ | 아키텍처 원칙 | `README.md` "Planned architecture" |
11
+ | 변경 이력 | `CHANGELOG.md` |
12
+
13
+ ## 규약
14
+
15
+ - **Django 가 연결·트랜잭션 주인이다.** SQLAlchemy `Engine`/`Connection`/`Session` 을 실행 경로에 두지 않는다. 실행은 항상 Django cursor. (2026-10-25 사전 프로브: SA 가 Django DBAPI 연결을 쥐면 `atomic()` 롤백이 무시됐다.)
16
+ - **SQLAlchemy 공개 API 를 우선한다.** `_` 접두 내부 속성 사용 금지. 예외: 외부 행으로 SA `Result` 를 만드는 경로(공개 생성법 없음, 2026-10-25 스파이크 판정)는 어댑터 모듈 한 곳에 가두고 SA 상한을 핀해 CI 로 지킨다.
17
+ - 스키마·마이그레이션은 Django 모델이 단일 출처.
18
+ - 커밋 메시지는 영어 Conventional Commits. 문서는 README 포함 한국어(마스터 지시 2026-10-25). README 의 수치는 `bench/` 결과 파일이 있는 것만 싣는다.
19
+ - 작업 큐는 GitHub 이슈다. 결함은 재현한 뒤에 말하고, 수정은 "수정 전에 실패하는 테스트"로 증명한다.
20
+ - 남의 저장소(aldjemy 포함)에는 이슈·코멘트·PR 을 만들지 않는다. 읽기만.
21
+ - 릴리스: `pyproject.toml` version 올림 → CHANGELOG 절 추가 → `python3 scripts/release_check.py` → `v<version>` 태그 푸시. `release.yml` 이 PyPI(trusted publishing, environment `pypi`)와 GitHub Release 에 올린다. 토큰은 저장하지 않는다.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 itda-work
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-sqlalchemy-bridge
3
+ Version: 0.0.1
4
+ Summary: [Experimental] Use SQLAlchemy 2.x Core expressions inside Django's own connections and transactions
5
+ Project-URL: Repository, https://github.com/itda-work/django-sqlalchemy-bridge
6
+ Author: itda-work
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 1 - Planning
10
+ Classifier: Framework :: Django
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Database
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: django>=5.2
20
+ Requires-Dist: sqlalchemy<2.2,>=2.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # django-sqlalchemy-bridge
24
+
25
+ > ⚠️ **실험적 프로젝트입니다 (Experimental).** API·동작·지원 범위가 예고 없이 바뀔 수 있고, 운영 환경에서 쓰는 것을 권하지 않습니다.
26
+ >
27
+ > **상태: 설계 단계 (0.0.x).** 이름을 확보하고 설계를 공개한 버전입니다. 아직 공개 API가 없으니 의존하지 마세요.
28
+
29
+ Django가 연 **연결과 트랜잭션 안에서** SQLAlchemy 2.x Core 표현식(대량 DML, CTE, 윈도 함수, 복잡한 조인)을 쓰게 해 주는 라이브러리입니다.
30
+
31
+ ## 왜 만드나
32
+
33
+ [aldjemy](https://github.com/aldjemy/aldjemy)가 같은 문제를 먼저 풀었지만, SQLAlchemy 내부 비공개 풀 구조에 기대 Django 연결을 공유하는 탓에 SQLAlchemy 2 미만에 묶여 있습니다. Django 6.0부터는 SQLAlchemy 버전과 상관없이 모델 변환 단계에서도 깨집니다. 이 프로젝트는 다른 길을 갑니다.
34
+
35
+ ## 설계 원칙
36
+
37
+ - **연결과 트랜잭션은 Django가 관리합니다.** SQLAlchemy는 SQL을 만들고 컴파일하는 데만 쓰고, 실행은 항상 Django 커서로 합니다. 그래서 `atomic()`, savepoint, `on_commit`, `execute_wrapper`, `assertNumQueries`, Django 연결 풀, PgBouncer 설정이 그대로 동작합니다.
38
+ - **SQLAlchemy 공개 API를 우선합니다.** 공개 API만으로 안 되는 부분(외부에서 받은 행으로 SQLAlchemy `Result`를 만드는 경로)은 어댑터 모듈 한 곳에 가두고, 지원하는 SQLAlchemy 버전 범위를 좁게 고정해 CI로 지킵니다.
39
+ - **테이블 구조의 기준은 Django 모델 하나입니다.** `sa.Table`은 Django 모델에서 만들고, 마이그레이션은 Django가 맡습니다.
40
+ - **값 변환은 Django 필드를 따릅니다.** 쓸 때는 `get_db_prep_value`, 읽을 때는 Django ORM이 쓰는 변환기를 거칩니다. 그래서 JSON·UUID·시간대가 있는 datetime·Decimal·Enum이 ORM과 똑같이 왕복합니다. (SQLAlchemy 타입 변환을 그대로 쓰면 SQLite에서 시간이 9시간 밀리거나 Enum이 값 대신 이름으로 저장되는 문제가 있어 피합니다.)
41
+ - **경계를 드러냅니다.** 브리지로 쓴 내용은 Django signal을 발생시키지 않습니다. API 이름과 문서에 이 점을 분명히 적습니다.
42
+
43
+ ## 비동기 (검토 중)
44
+
45
+ Django의 async ORM(`aget()` 등)은 내부적으로 쿼리마다 `sync_to_async`로 스레드에 넘기는 구조이고, Django 문서도 "async 모드에서는 아직 트랜잭션이 동작하지 않는다"고 적고 있습니다.
46
+
47
+ - **v0.1 방향(Django만으로):** 여러 쿼리와 `atomic()`을 묶어 스레드에 한 번만 넘기는 `atransaction()`·`aexecute()`를 제공합니다. 트랜잭션을 Django가 관리한다는 원칙을 지키면서 async 코드에서도 트랜잭션을 쓸 수 있게 합니다.
48
+ - **검토 중인 대안:** SQLAlchemy `AsyncEngine`(별도 연결, 읽기 전용으로 명시적으로 켠 경우만)과 psycopg 3 비동기 연결 직접 사용. PostgreSQL 측정 결과를 보고 정합니다.
49
+
50
+ ## 성능과 메모리 (측정 중)
51
+
52
+ 이 README의 모든 수치는 저장소의 `bench/` 스크립트와 결과 파일로 다시 재현할 수 있을 때만 싣습니다. 측정할 항목은 아래와 같습니다.
53
+
54
+ - PK 기준 대량 UPDATE: Django `bulk_update`, django-pgbulk, 브리지, SQLAlchemy ORM
55
+ - 대량 INSERT: `bulk_create`, pgbulk `copy`, 브리지, SQLAlchemy `insert()`
56
+ - 대량 읽기의 시간과 메모리: Django 모델 객체, `values()`, `values_list()`, 브리지, SQLAlchemy `Row`, SQLAlchemy ORM 객체
57
+ - 쿼리 생성 오버헤드, async 경로, CTE·윈도 함수 리포트 쿼리
58
+
59
+ 브리지가 도움이 **안 되는** 경우(일반 CRUD, django-pgbulk로 충분한 경우)도 함께 적습니다.
60
+
61
+ ## 첫 지원 대상 (계획)
62
+
63
+ SQLite, PostgreSQL(psycopg 3) · Django 5.2 이상 · SQLAlchemy 2.0 이상 2.2 미만 · Python 3.12 이상
64
+
65
+ ## 라이선스
66
+
67
+ MIT
@@ -0,0 +1,45 @@
1
+ # django-sqlalchemy-bridge
2
+
3
+ > ⚠️ **실험적 프로젝트입니다 (Experimental).** API·동작·지원 범위가 예고 없이 바뀔 수 있고, 운영 환경에서 쓰는 것을 권하지 않습니다.
4
+ >
5
+ > **상태: 설계 단계 (0.0.x).** 이름을 확보하고 설계를 공개한 버전입니다. 아직 공개 API가 없으니 의존하지 마세요.
6
+
7
+ Django가 연 **연결과 트랜잭션 안에서** SQLAlchemy 2.x Core 표현식(대량 DML, CTE, 윈도 함수, 복잡한 조인)을 쓰게 해 주는 라이브러리입니다.
8
+
9
+ ## 왜 만드나
10
+
11
+ [aldjemy](https://github.com/aldjemy/aldjemy)가 같은 문제를 먼저 풀었지만, SQLAlchemy 내부 비공개 풀 구조에 기대 Django 연결을 공유하는 탓에 SQLAlchemy 2 미만에 묶여 있습니다. Django 6.0부터는 SQLAlchemy 버전과 상관없이 모델 변환 단계에서도 깨집니다. 이 프로젝트는 다른 길을 갑니다.
12
+
13
+ ## 설계 원칙
14
+
15
+ - **연결과 트랜잭션은 Django가 관리합니다.** SQLAlchemy는 SQL을 만들고 컴파일하는 데만 쓰고, 실행은 항상 Django 커서로 합니다. 그래서 `atomic()`, savepoint, `on_commit`, `execute_wrapper`, `assertNumQueries`, Django 연결 풀, PgBouncer 설정이 그대로 동작합니다.
16
+ - **SQLAlchemy 공개 API를 우선합니다.** 공개 API만으로 안 되는 부분(외부에서 받은 행으로 SQLAlchemy `Result`를 만드는 경로)은 어댑터 모듈 한 곳에 가두고, 지원하는 SQLAlchemy 버전 범위를 좁게 고정해 CI로 지킵니다.
17
+ - **테이블 구조의 기준은 Django 모델 하나입니다.** `sa.Table`은 Django 모델에서 만들고, 마이그레이션은 Django가 맡습니다.
18
+ - **값 변환은 Django 필드를 따릅니다.** 쓸 때는 `get_db_prep_value`, 읽을 때는 Django ORM이 쓰는 변환기를 거칩니다. 그래서 JSON·UUID·시간대가 있는 datetime·Decimal·Enum이 ORM과 똑같이 왕복합니다. (SQLAlchemy 타입 변환을 그대로 쓰면 SQLite에서 시간이 9시간 밀리거나 Enum이 값 대신 이름으로 저장되는 문제가 있어 피합니다.)
19
+ - **경계를 드러냅니다.** 브리지로 쓴 내용은 Django signal을 발생시키지 않습니다. API 이름과 문서에 이 점을 분명히 적습니다.
20
+
21
+ ## 비동기 (검토 중)
22
+
23
+ Django의 async ORM(`aget()` 등)은 내부적으로 쿼리마다 `sync_to_async`로 스레드에 넘기는 구조이고, Django 문서도 "async 모드에서는 아직 트랜잭션이 동작하지 않는다"고 적고 있습니다.
24
+
25
+ - **v0.1 방향(Django만으로):** 여러 쿼리와 `atomic()`을 묶어 스레드에 한 번만 넘기는 `atransaction()`·`aexecute()`를 제공합니다. 트랜잭션을 Django가 관리한다는 원칙을 지키면서 async 코드에서도 트랜잭션을 쓸 수 있게 합니다.
26
+ - **검토 중인 대안:** SQLAlchemy `AsyncEngine`(별도 연결, 읽기 전용으로 명시적으로 켠 경우만)과 psycopg 3 비동기 연결 직접 사용. PostgreSQL 측정 결과를 보고 정합니다.
27
+
28
+ ## 성능과 메모리 (측정 중)
29
+
30
+ 이 README의 모든 수치는 저장소의 `bench/` 스크립트와 결과 파일로 다시 재현할 수 있을 때만 싣습니다. 측정할 항목은 아래와 같습니다.
31
+
32
+ - PK 기준 대량 UPDATE: Django `bulk_update`, django-pgbulk, 브리지, SQLAlchemy ORM
33
+ - 대량 INSERT: `bulk_create`, pgbulk `copy`, 브리지, SQLAlchemy `insert()`
34
+ - 대량 읽기의 시간과 메모리: Django 모델 객체, `values()`, `values_list()`, 브리지, SQLAlchemy `Row`, SQLAlchemy ORM 객체
35
+ - 쿼리 생성 오버헤드, async 경로, CTE·윈도 함수 리포트 쿼리
36
+
37
+ 브리지가 도움이 **안 되는** 경우(일반 CRUD, django-pgbulk로 충분한 경우)도 함께 적습니다.
38
+
39
+ ## 첫 지원 대상 (계획)
40
+
41
+ SQLite, PostgreSQL(psycopg 3) · Django 5.2 이상 · SQLAlchemy 2.0 이상 2.2 미만 · Python 3.12 이상
42
+
43
+ ## 라이선스
44
+
45
+ MIT
@@ -0,0 +1,13 @@
1
+ """django-sqlalchemy-bridge — design stage, no public API yet.
2
+
3
+ See README.md for the planned architecture.
4
+ """
5
+
6
+ from importlib.metadata import PackageNotFoundError, version
7
+
8
+ try:
9
+ __version__ = version("django-sqlalchemy-bridge")
10
+ except PackageNotFoundError: # pragma: no cover - running from a source tree without install
11
+ __version__ = "0.0.0"
12
+
13
+ __all__ = ["__version__"]
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "django-sqlalchemy-bridge"
3
+ version = "0.0.1"
4
+ description = "[Experimental] Use SQLAlchemy 2.x Core expressions inside Django's own connections and transactions"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ authors = [{ name = "itda-work" }]
10
+ classifiers = [
11
+ "Development Status :: 1 - Planning",
12
+ "Framework :: Django",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Topic :: Database",
20
+ ]
21
+ dependencies = [
22
+ "django>=5.2",
23
+ # <2.2: building a Result from external rows has no public constructor; widen only after CI checks the new minor.
24
+ "sqlalchemy>=2.0,<2.2",
25
+ ]
26
+
27
+ [project.urls]
28
+ Repository = "https://github.com/itda-work/django-sqlalchemy-bridge"
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["django_sqlalchemy_bridge"]
36
+
37
+ [tool.ruff]
38
+ line-length = 120
39
+ target-version = "py312"
40
+
41
+ [tool.ruff.lint]
42
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,62 @@
1
+ """Refuse a release whose version is not declared everywhere it has to be.
2
+
3
+ Run this before tagging; release.yml runs it again with
4
+ the tag it is building. (Copied from itda-work/channels-nats.)
5
+
6
+ Deliberately dependency-free and stdlib-only, so it runs on any Python this
7
+ project supports, including 3.10 without tomllib.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ ROOT = Path(__file__).resolve().parent.parent
17
+
18
+
19
+ def declared_version() -> str:
20
+ """The one `version = "..."` line pyproject.toml is allowed to have."""
21
+ text = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
22
+ found = re.findall(r'^version = "(.+?)"$', text, re.MULTILINE)
23
+ if len(found) != 1:
24
+ raise SystemExit(f"pyproject.toml has {len(found)} version lines; expected exactly one")
25
+ return found[0]
26
+
27
+
28
+ def problems(tag: str | None) -> list[str]:
29
+ version = declared_version()
30
+ pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
31
+ changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
32
+ found = []
33
+
34
+ if tag is not None and tag != f"v{version}":
35
+ found.append(f"tag {tag} does not match pyproject version {version}")
36
+
37
+ if re.search(r'^\s*"License :: ', pyproject, re.MULTILINE):
38
+ # PEP 639: PyPI rejects an upload carrying both. `twine check` does not catch it.
39
+ found.append("drop the 'License :: ...' classifiers; they cannot accompany License-Expression")
40
+
41
+ if f"## [{version}]" not in changelog:
42
+ found.append(f"CHANGELOG.md has no '## [{version}]' section")
43
+
44
+ unreleased = re.search(r"^## \[Unreleased\]\s*\n(.*?)(?=^## |\Z)", changelog, re.MULTILINE | re.DOTALL)
45
+ if unreleased is not None and unreleased.group(1).strip():
46
+ found.append(f"CHANGELOG.md still has entries under [Unreleased]; they belong in [{version}]")
47
+
48
+ return found
49
+
50
+
51
+ def main(argv: list[str]) -> int:
52
+ found = problems(argv[0] if argv else None)
53
+ for problem in found:
54
+ print(f"release check: {problem}", file=sys.stderr)
55
+ if found:
56
+ return 1
57
+ print(f"release check: {declared_version()} is consistent")
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main(sys.argv[1:]))