django-sqlite-ops 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ """권장 SQLite 설정 표와 표준 ``DATABASES`` 항목 생성 함수 (DESIGN §6-0).
2
+
3
+ ``settings.py`` 에서 부르는 모듈이라 표준 라이브러리만 import 한다. Django 를 import 하지 않는다.
4
+ """
5
+
6
+ import re
7
+ from collections.abc import Mapping
8
+ from os import PathLike
9
+ from types import MappingProxyType
10
+ from typing import Any
11
+
12
+ __all__ = ["DEFAULT_PROFILE", "PROFILES", "RECOMMENDED", "recommended", "sqlite_database"]
13
+
14
+ ENGINE = "django.db.backends.sqlite3"
15
+ DEFAULT_PROFILE = "single-server"
16
+
17
+ # 권장값의 정본. 실측 근거가 있는 값만 넣는다(DESIGN §6-0 표의 "기본 적용 = 켬").
18
+ # foreign_keys 는 Django 가 이미 켜고, wal_autocheckpoint 는 Litestream 이 관리하므로 넣지 않는다.
19
+ _SINGLE_SERVER = MappingProxyType(
20
+ {
21
+ "transaction_mode": "IMMEDIATE",
22
+ "pragmas": MappingProxyType(
23
+ {
24
+ "journal_mode": "WAL",
25
+ "synchronous": "NORMAL",
26
+ "busy_timeout": 5000,
27
+ }
28
+ ),
29
+ }
30
+ )
31
+
32
+ RECOMMENDED: Mapping[str, Mapping[str, Any]] = MappingProxyType(
33
+ {
34
+ "single-server": _SINGLE_SERVER,
35
+ # v0.1 에서는 DB 설정이 같다. 프로필별로 달라질 수 있게 따로 둔다.
36
+ "single-server-multiproc": _SINGLE_SERVER,
37
+ }
38
+ )
39
+ PROFILES = tuple(RECOMMENDED)
40
+
41
+ _IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
42
+ # Django 는 init_command 를 ";" 로 나눠 실행한다. 값에 ";" 나 공백·따옴표가 들어오지 못하게 한다.
43
+ _PRAGMA_VALUE = re.compile(r"-?[A-Za-z0-9_]+")
44
+
45
+
46
+ def recommended(profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
47
+ """프로필의 권장값을 복사해서 돌려준다: ``{"transaction_mode": ..., "pragmas": {...}}``."""
48
+ try:
49
+ entry = RECOMMENDED[profile]
50
+ except KeyError:
51
+ raise ValueError(
52
+ f"unknown profile {profile!r}; expected one of {', '.join(PROFILES)}"
53
+ ) from None
54
+ return {"transaction_mode": entry["transaction_mode"], "pragmas": dict(entry["pragmas"])}
55
+
56
+
57
+ def _check_pragma_name(name: Any) -> None:
58
+ if not isinstance(name, str) or not _IDENTIFIER.fullmatch(name):
59
+ raise ValueError(f"invalid PRAGMA name {name!r}; must be an SQL identifier")
60
+
61
+
62
+ def _format_pragma(name: str, value: Any) -> str:
63
+ if isinstance(value, bool):
64
+ text = "ON" if value else "OFF"
65
+ elif isinstance(value, int):
66
+ text = str(value)
67
+ elif isinstance(value, str) and _PRAGMA_VALUE.fullmatch(value):
68
+ text = value
69
+ else:
70
+ raise ValueError(
71
+ f"invalid value {value!r} for PRAGMA {name}; use an int, bool or bare keyword"
72
+ )
73
+ return f"PRAGMA {name}={text}"
74
+
75
+
76
+ def sqlite_database(
77
+ name: str | PathLike[str],
78
+ *,
79
+ profile: str = DEFAULT_PROFILE,
80
+ pragmas: Mapping[str, Any] | None = None,
81
+ options: Mapping[str, Any] | None = None,
82
+ ) -> dict[str, Any]:
83
+ """권장값을 담은 ``DATABASES`` 항목을 돌려준다.
84
+
85
+ ``pragmas`` 는 PRAGMA 를 덮거나 더한다. 값이 ``None`` 이면 그 PRAGMA 를 뺀다.
86
+ PRAGMA 이름은 SQLite 처럼 대소문자를 가리지 않고 소문자로 맞춘다.
87
+ ``options`` 는 ``OPTIONS`` 키를 더하거나 덮는다. ``init_command`` 는 ``pragmas`` 로만 정한다.
88
+
89
+ ``options={"timeout": N}``(초)만 주면 대기 시간이 바뀌지 않는다. ``init_command`` 의
90
+ ``busy_timeout``(밀리초, 기본 5000)이 연결 뒤에 실행되어 우선하기 때문이다. ``timeout`` 을
91
+ 쓰려면 ``pragmas={"busy_timeout": None}`` 을 함께 주고, 아니면 ``busy_timeout`` 을 덮는다.
92
+ """
93
+ rec = recommended(profile)
94
+ if options and "init_command" in options:
95
+ raise ValueError(
96
+ "options['init_command'] conflicts with the generated PRAGMAs; "
97
+ "use pragmas={...} instead"
98
+ )
99
+
100
+ merged = rec["pragmas"]
101
+ seen: set[str] = set()
102
+ for key, value in (pragmas or {}).items():
103
+ _check_pragma_name(key)
104
+ key = key.lower()
105
+ if key in seen:
106
+ raise ValueError(f"PRAGMA {key} given more than once in pragmas (names ignore case)")
107
+ seen.add(key)
108
+ if value is None:
109
+ merged.pop(key, None)
110
+ else:
111
+ merged[key] = value
112
+ statements = [_format_pragma(key, value) for key, value in merged.items()]
113
+
114
+ result_options: dict[str, Any] = {"transaction_mode": rec["transaction_mode"]}
115
+ if statements:
116
+ result_options["init_command"] = ";".join(statements)
117
+ result_options.update(options or {})
118
+ return {"ENGINE": ENGINE, "NAME": str(name), "OPTIONS": result_options}