jetstream-api 0.21.12__tar.gz → 0.22.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 (32) hide show
  1. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/PKG-INFO +21 -2
  2. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/README.md +20 -1
  3. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/__init__.py +3 -1
  4. jetstream_api-0.22.0/ilink/_propv2.py +440 -0
  5. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/admin.py +212 -96
  6. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/admintopic.py +3 -5
  7. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/common.py +6 -6
  8. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/properties.py +297 -125
  9. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/qmgr.py +19 -20
  10. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/topic.py +4 -6
  11. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/jetstream_api.egg-info/PKG-INFO +21 -2
  12. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/jetstream_api.egg-info/SOURCES.txt +1 -0
  13. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/pyproject.toml +1 -1
  14. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/LICENSE.txt +0 -0
  15. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/_codec.py +0 -0
  16. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/_consumer.py +0 -0
  17. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/_groupwire.py +0 -0
  18. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/_qleader.py +0 -0
  19. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/_throttle.py +0 -0
  20. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/connectivity.py +0 -0
  21. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/exception.py +0 -0
  22. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/msg.py +0 -0
  23. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/partitioner.py +0 -0
  24. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/pattern.py +0 -0
  25. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/producer.py +0 -0
  26. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/py.typed +0 -0
  27. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/ilink/util.py +0 -0
  28. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/jetstream_api.egg-info/dependency_links.txt +0 -0
  29. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/jetstream_api.egg-info/requires.txt +0 -0
  30. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/jetstream_api.egg-info/top_level.txt +0 -0
  31. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/setup.cfg +0 -0
  32. {jetstream_api-0.21.12 → jetstream_api-0.22.0}/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.21.12
3
+ Version: 0.22.0
4
4
  Summary: Python client API for iLink M.O.M. (Message Oriented Middleware)
5
5
  Author: snowjeans
6
6
  License: Boost Software License - Version 1.0 - August 17th, 2003
@@ -60,6 +60,25 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.22.0 변경 - 7 신규 오브젝트(쿼터 · 디스크 가드 · 토픽 · 클러스터) 관리를 Property v2(JSON)로 (자바 v2.18.0 와 같음, 짝 엔진 7.0.1.3601 이상)
64
+
65
+ - **와이어**: 쿼터 · 디스크 가드 · 토픽 · 클러스터의 생성 · 조회 · 목록 · 변경 · 삭제가 새 요청 코드 3600~3604(본문 = UTF-8 JSON)로 간다.
66
+ ★ 이 네 오브젝트는 **엔진 7.0.1.3601 이상에서만** 된다(엔진이 옛 코드를 지웠다). 6.x 엔진에는 원래 없는 기능이라 보내지 않고
67
+ ``MIMQE_NOT_SUPPORTED (... requires ILink 7)`` 로 거절한다. 큐 · 채널 · 큐 관리자 · 세션 등 6.x 오브젝트와 데이터 경로는 바뀌지 않았다.
68
+ - **바꾼 값만 보낸다(patch)**: ``setQuotaProperty`` · ``setDiskGuardProperty`` · ``setClusterProperty`` 는 **세터(또는 필드 대입)로 바꾼
69
+ 칸만** 보낸다. 조회한 객체를 그대로 넘기면 아무것도 보내지 않는다. 쿼터 클라 항목 · 클러스터 노드 목록은 바뀌면 통째로 간다.
70
+ 클러스터는 읽은 정의 판(defTerm, defVersion)을 낙관적 잠금으로 자동으로 싣는다.
71
+ - **새 함수 - 결과를 돌려준다**: ``setQuotaPropertyWithResult`` · ``setDiskGuardPropertyWithResult`` · ``setClusterPropertyWithResult`` ·
72
+ ``createTopicWithResult`` · ``setTopicPropertyWithResult`` · ``removeTopicWithResult`` -> :class:`ILPropertyResult`(적용된 키와 반영 시점 ·
73
+ 무시된 키와 까닭 · 바꿔 받은 값 · 조작별 결과). 무시된 키가 있으면 기존 함수도 WARNING 로그(``ilink.propv2``)를 남긴다.
74
+ - **동작 변화**: 토픽 옵션 문자열의 모르는 키는 거절되지 않고 **무시 + 경고**가 된다(모르는 키 무시 규칙). 범위 밖 토픽 값(SEGMENTBYTES 등)은
75
+ 엔진이 고쳐 받지 않고 **거절**한다. 없는 토픽 조회는 ``MIMQE_OBJECT_NOT_FOUND``. 토픽 목록(``getTopicList`` · ``list_topics``)은 예전과 같은
76
+ ``name|earliest|next|epoch|partitions`` 줄로 돌려준다.
77
+ - **없앤 것(옛 와이어 코덱)**: ``ILQuotaProperty(text)`` · ``to_text``, ``ILDiskGuardProperty(csv)`` · ``to_csv``. 클러스터의 옛 블롭 코덱은
78
+ 아직 남아 있으나 쓰지 않는다(다음 정리 대상).
79
+ - 문서: 트랜잭션 커밋이 ``MIMQE_FOLLOWER_NODE`` 로 실패하면 적용 안 됨 - 처음부터 다시(엔진 r3598). 클러스터 노드 번호는 1~62.
80
+ 엔진 판 표기 ``v7.1.x`` 를 엔진 리비전(rNNNN)으로.
81
+
63
82
  ## 0.21.12 변경 - 긴 실패 사유를 자르지 않는다 (자바 v2.17.12 와 같음, 짝 엔진 7.0.1.3597 이상)
64
83
 
65
84
  - **실패 사유 전문**: 응답의 사유 칸은 69바이트라 긴 사유가 잘렸다. 엔진 7.0.1.3597 부터는 그보다 긴 사유의 전문(최대 8192 바이트,
@@ -972,7 +991,7 @@ svc.createCluster(c)
972
991
  큐에 더해 **pub/sub 토픽**을 지원합니다. 발행된 메시지는 retention 정책이 지울 때까지 보존되어
973
992
  모든 구독자에게 각자의 커서로 전달됩니다(팬아웃). 큐와 달리 소비해도 사라지지 않습니다.
974
993
 
975
- > 엔진 v7.1.1 rev 3265 이상이 필요합니다.
994
+ > 엔진 r3265 이상이 필요합니다.
976
995
 
977
996
  ### 구독
978
997
 
@@ -10,6 +10,25 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
10
10
  ## 설치 방법
11
11
  pip install jetstream-api
12
12
 
13
+ ## 0.22.0 변경 - 7 신규 오브젝트(쿼터 · 디스크 가드 · 토픽 · 클러스터) 관리를 Property v2(JSON)로 (자바 v2.18.0 와 같음, 짝 엔진 7.0.1.3601 이상)
14
+
15
+ - **와이어**: 쿼터 · 디스크 가드 · 토픽 · 클러스터의 생성 · 조회 · 목록 · 변경 · 삭제가 새 요청 코드 3600~3604(본문 = UTF-8 JSON)로 간다.
16
+ ★ 이 네 오브젝트는 **엔진 7.0.1.3601 이상에서만** 된다(엔진이 옛 코드를 지웠다). 6.x 엔진에는 원래 없는 기능이라 보내지 않고
17
+ ``MIMQE_NOT_SUPPORTED (... requires ILink 7)`` 로 거절한다. 큐 · 채널 · 큐 관리자 · 세션 등 6.x 오브젝트와 데이터 경로는 바뀌지 않았다.
18
+ - **바꾼 값만 보낸다(patch)**: ``setQuotaProperty`` · ``setDiskGuardProperty`` · ``setClusterProperty`` 는 **세터(또는 필드 대입)로 바꾼
19
+ 칸만** 보낸다. 조회한 객체를 그대로 넘기면 아무것도 보내지 않는다. 쿼터 클라 항목 · 클러스터 노드 목록은 바뀌면 통째로 간다.
20
+ 클러스터는 읽은 정의 판(defTerm, defVersion)을 낙관적 잠금으로 자동으로 싣는다.
21
+ - **새 함수 - 결과를 돌려준다**: ``setQuotaPropertyWithResult`` · ``setDiskGuardPropertyWithResult`` · ``setClusterPropertyWithResult`` ·
22
+ ``createTopicWithResult`` · ``setTopicPropertyWithResult`` · ``removeTopicWithResult`` -> :class:`ILPropertyResult`(적용된 키와 반영 시점 ·
23
+ 무시된 키와 까닭 · 바꿔 받은 값 · 조작별 결과). 무시된 키가 있으면 기존 함수도 WARNING 로그(``ilink.propv2``)를 남긴다.
24
+ - **동작 변화**: 토픽 옵션 문자열의 모르는 키는 거절되지 않고 **무시 + 경고**가 된다(모르는 키 무시 규칙). 범위 밖 토픽 값(SEGMENTBYTES 등)은
25
+ 엔진이 고쳐 받지 않고 **거절**한다. 없는 토픽 조회는 ``MIMQE_OBJECT_NOT_FOUND``. 토픽 목록(``getTopicList`` · ``list_topics``)은 예전과 같은
26
+ ``name|earliest|next|epoch|partitions`` 줄로 돌려준다.
27
+ - **없앤 것(옛 와이어 코덱)**: ``ILQuotaProperty(text)`` · ``to_text``, ``ILDiskGuardProperty(csv)`` · ``to_csv``. 클러스터의 옛 블롭 코덱은
28
+ 아직 남아 있으나 쓰지 않는다(다음 정리 대상).
29
+ - 문서: 트랜잭션 커밋이 ``MIMQE_FOLLOWER_NODE`` 로 실패하면 적용 안 됨 - 처음부터 다시(엔진 r3598). 클러스터 노드 번호는 1~62.
30
+ 엔진 판 표기 ``v7.1.x`` 를 엔진 리비전(rNNNN)으로.
31
+
13
32
  ## 0.21.12 변경 - 긴 실패 사유를 자르지 않는다 (자바 v2.17.12 와 같음, 짝 엔진 7.0.1.3597 이상)
14
33
 
15
34
  - **실패 사유 전문**: 응답의 사유 칸은 69바이트라 긴 사유가 잘렸다. 엔진 7.0.1.3597 부터는 그보다 긴 사유의 전문(최대 8192 바이트,
@@ -922,7 +941,7 @@ svc.createCluster(c)
922
941
  큐에 더해 **pub/sub 토픽**을 지원합니다. 발행된 메시지는 retention 정책이 지울 때까지 보존되어
923
942
  모든 구독자에게 각자의 커서로 전달됩니다(팬아웃). 큐와 달리 소비해도 사라지지 않습니다.
924
943
 
925
- > 엔진 v7.1.1 rev 3265 이상이 필요합니다.
944
+ > 엔진 r3265 이상이 필요합니다.
926
945
 
927
946
  ### 구독
928
947
 
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.21.12"
3
+ __version__ = "0.22.0"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -58,6 +58,7 @@ from .properties import (
58
58
  ILQuotaMeter,
59
59
  ILQuotaFollower,
60
60
  ILQuotaRefusedPeer,
61
+ ILPropertyResult,
61
62
  ILThrottleStats,
62
63
  )
63
64
 
@@ -131,6 +132,7 @@ __all__ = [
131
132
  "ILQuotaMeter",
132
133
  "ILQuotaFollower",
133
134
  "ILQuotaRefusedPeer",
135
+ "ILPropertyResult",
134
136
  "ILThrottleStats",
135
137
  # Exceptions
136
138
  "ILException",
@@ -0,0 +1,440 @@
1
+ """Property v2 속성 와이어 - JSON 본문 규칙(모듈 내부용, 엔진 PROPV2-1).
2
+
3
+ 본문은 UTF-8 JSON 객체 하나다(BOM 없음). 오브젝트 속성은 최상위에 평평하게 싣고, ``_`` 로 시작하는 키는 메타다
4
+ (``_TYPE`` · ``_KEYS`` · ``_ITEMS`` · ``_APPLIED`` · ``_IGNORED`` · ``_NORMALIZED`` · ``_RESULT`` · ``_EXPECT``).
5
+ 정본은 엔진 ``DESIGN_PROPERTY_V2.md`` §3 이고, 세 언어가 같은 시험 벡터(``propv2_test/json_rule_vectors.txt``)로 맞춘다.
6
+
7
+ 파서는 엄격하다 - 깨진 UTF-8 · 중복 키(중첩 포함) · 깊이 8 초과 · null · 섞인 배열 · 문자열 안 NUL · 뒤 쓰레기 ·
8
+ 비표준 확장(주석 · 끝 쉼표 · NaN · 작은따옴표 · 앞자리 0)은 거절한다. 키는 ``[A-Z0-9_]`` 1~63자, 첫 글자 ``[A-Z_]``.
9
+
10
+ 값 종류(:func:`take`): ``INT`` = 정수 number(int32), ``LONG`` = 10진 문자열(int64, ``-?[0-9]+``), ``DOUBLE`` = number,
11
+ ``BOOL`` = true/false, ``STR`` = string.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import re
19
+
20
+ MAX_KEY_LEN = 63
21
+ MAX_DEPTH = 8 # 최상위 객체 = 0, 안쪽 객체 / 배열 한 겹마다 +1
22
+
23
+ _INT_MIN, _INT_MAX = -(1 << 31), (1 << 31) - 1
24
+ _LONG_MIN, _LONG_MAX = -(1 << 63), (1 << 63) - 1
25
+ _LONG_RE = re.compile(r"-?[0-9]+\Z")
26
+ _KEY_RE = re.compile(r"[A-Z_][A-Z0-9_]*\Z")
27
+ _WS = " \t\r\n"
28
+
29
+
30
+ class PropV2Error(ValueError):
31
+ """본문 규칙 위반."""
32
+
33
+
34
+ def _no_constant(name: str):
35
+ raise PropV2Error("JSON: %s is not allowed" % name)
36
+
37
+
38
+ def _pairs(pairs):
39
+ # 중복 키를 그 자리에서 잡는다(경로는 아래 검사가 붙인다) - 순서는 보존한다
40
+ d = {}
41
+ for k, v in pairs:
42
+ if k in d:
43
+ raise PropV2Error("duplicate key %s" % k)
44
+ d[k] = v
45
+ return d
46
+
47
+
48
+ def is_valid_key(k: str) -> bool:
49
+ """키 문자 규칙(1~63자, ``[A-Z0-9_]``, 첫 글자 ``[A-Z_]``)에 맞는가.
50
+
51
+ Returns:
52
+ bool: 맞으면 True.
53
+ """
54
+ return bool(k) and len(k) <= MAX_KEY_LEN and _KEY_RE.match(k) is not None
55
+
56
+
57
+ def _kind(v) -> str:
58
+ if isinstance(v, bool):
59
+ return "b"
60
+ if isinstance(v, (int, float)):
61
+ return "n"
62
+ if isinstance(v, str):
63
+ return "s"
64
+ if isinstance(v, dict):
65
+ return "o"
66
+ if isinstance(v, list):
67
+ return "a"
68
+ return "?"
69
+
70
+
71
+ def _check(v, path: str, depth: int) -> None:
72
+ if depth > MAX_DEPTH:
73
+ raise PropV2Error("nesting deeper than 8")
74
+ if v is None:
75
+ raise PropV2Error("%s must not be null" % (path or "value"))
76
+ if isinstance(v, float) and (v != v or v in (float("inf"), float("-inf"))):
77
+ raise PropV2Error("JSON: number out of range")
78
+ if isinstance(v, str):
79
+ if "\x00" in v:
80
+ raise PropV2Error("%s contains NUL" % path)
81
+ try:
82
+ v.encode("utf-8")
83
+ except UnicodeEncodeError:
84
+ raise PropV2Error("JSON: lone surrogate in %s" % path) from None
85
+ elif isinstance(v, dict):
86
+ for k, x in v.items():
87
+ if not is_valid_key(k):
88
+ raise PropV2Error("bad key '%s'" % k)
89
+ _check(x, k if not path else path + "." + k, depth + 1)
90
+ elif isinstance(v, list):
91
+ kinds = set()
92
+ for i, x in enumerate(v):
93
+ if x is None:
94
+ raise PropV2Error("%s[%d] must not be null" % (path, i))
95
+ kinds.add(_kind(x))
96
+ _check(x, "%s[%d]" % (path, i), depth + 1)
97
+ if len(kinds) > 1:
98
+ raise PropV2Error("%s mixes element types" % path)
99
+
100
+
101
+ def parse(data: bytes | bytearray | memoryview) -> dict:
102
+ """본문 바이트를 객체(dict, 키 순서 보존)로 푼다.
103
+
104
+ Returns:
105
+ dict: 최상위 객체.
106
+
107
+ Raises:
108
+ PropV2Error: 규칙 위반(부분 결과 없음).
109
+ """
110
+ b = bytes(data)
111
+ try:
112
+ text = b.decode("utf-8")
113
+ except UnicodeDecodeError:
114
+ raise PropV2Error("JSON: invalid UTF-8") from None
115
+ if not text.strip(_WS):
116
+ raise PropV2Error("JSON: empty body")
117
+ try:
118
+ obj = json.loads(text, object_pairs_hook=_pairs, parse_constant=_no_constant)
119
+ except PropV2Error as e:
120
+ if str(e).startswith("duplicate key"):
121
+ raise PropV2Error(_dup_path(text) or str(e)) from None
122
+ raise
123
+ except RecursionError:
124
+ raise PropV2Error("nesting deeper than 8") from None
125
+ except ValueError as e:
126
+ raise PropV2Error("JSON: %s" % e) from None
127
+ if not isinstance(obj, dict):
128
+ raise PropV2Error("must be an object")
129
+ _check(obj, "", 0)
130
+ return obj
131
+
132
+
133
+ def _dup_path(text: str) -> str:
134
+ # 중복 키의 경로(O.X · L[1].X)를 사유에 싣는다 - 다시 풀며 경로를 따라간다(실패 경로에서만 돈다)
135
+ found = []
136
+
137
+ def walk(pairs_list, path):
138
+ seen = set()
139
+ for k, v in pairs_list:
140
+ p = k if not path else path + "." + k
141
+ if k in seen:
142
+ found.append(p)
143
+ return
144
+ seen.add(k)
145
+
146
+ def hook(pairs):
147
+ return ("__pairs__", pairs)
148
+
149
+ try:
150
+ raw = json.loads(text, object_pairs_hook=hook, parse_constant=_no_constant)
151
+ except Exception:
152
+ return ""
153
+
154
+ def visit(node, path):
155
+ if found:
156
+ return
157
+ if isinstance(node, tuple) and node and node[0] == "__pairs__":
158
+ walk(node[1], path)
159
+ for k, v in node[1]:
160
+ visit(v, k if not path else path + "." + k)
161
+ elif isinstance(node, list):
162
+ for i, x in enumerate(node):
163
+ visit(x, "%s[%d]" % (path, i))
164
+
165
+ visit(raw, "")
166
+ return "duplicate key " + found[0] if found else ""
167
+
168
+
169
+ def take(obj: dict, key: str, kind: str):
170
+ """객체에서 키 하나를 그 종류로 꺼낸다.
171
+
172
+ Args:
173
+ obj (dict): :func:`parse` 결과(또는 그 안쪽 객체).
174
+ key (str): 키.
175
+ kind (str): 값 종류. ``"INT"`` · ``"LONG"`` · ``"DOUBLE"`` · ``"BOOL"`` · ``"STR"`` 중 하나.
176
+
177
+ Returns:
178
+ int | float | bool | str | None: 값. 키가 없으면 None.
179
+
180
+ Raises:
181
+ PropV2Error: 값이 그 종류가 아닐 때(``<KEY> must be LONG (decimal string)`` 등).
182
+ """
183
+ if key not in obj:
184
+ return None
185
+ v = obj[key]
186
+ if kind == "INT":
187
+ if type(v) is int and _INT_MIN <= v <= _INT_MAX:
188
+ return v
189
+ elif kind == "LONG":
190
+ if type(v) is str and _LONG_RE.match(v):
191
+ n = int(v)
192
+ if _LONG_MIN <= n <= _LONG_MAX:
193
+ return n
194
+ raise PropV2Error("%s must be LONG (decimal string)" % key)
195
+ elif kind == "DOUBLE":
196
+ if type(v) in (int, float):
197
+ return float(v)
198
+ elif kind == "BOOL":
199
+ if type(v) is bool:
200
+ return v
201
+ elif kind == "STR":
202
+ if type(v) is str:
203
+ return v
204
+ else:
205
+ raise ValueError("unknown kind %r" % kind)
206
+ raise PropV2Error("%s must be %s" % (key, kind))
207
+
208
+
209
+ def long_str(n: int) -> str:
210
+ """LONG 값을 와이어 표기(10진 문자열)로.
211
+
212
+ Returns:
213
+ str: 10진 문자열.
214
+ """
215
+ return "%d" % int(n)
216
+
217
+
218
+ def dumps(obj: dict) -> bytes:
219
+ """객체를 본문 바이트(UTF-8, 공백 없는 JSON)로. 보내기 전에 규칙을 같은 검사로 확인한다.
220
+
221
+ Returns:
222
+ bytes: 본문 바이트.
223
+
224
+ Raises:
225
+ PropV2Error: 규칙 위반(키 · null · 섞인 배열 · 깊이 등).
226
+ """
227
+ if not isinstance(obj, dict):
228
+ raise PropV2Error("must be an object")
229
+ _check(obj, "", 0)
230
+ return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")
231
+
232
+
233
+ # ── 요청(엔진 설계 §5-1) ───────────────────────────────────────────────────────
234
+
235
+ PROP_CREATE = 3600
236
+ PROP_ALTER = 3601 # 코드 자체가 patch 다(본문에 표지 없음)
237
+ PROP_GET = 3602
238
+ PROP_LIST = 3603
239
+ PROP_DELETE = 3604
240
+ PROP_FAILED = 4601 # 실패 REPORT(arg2 = 사유). 성공은 LONG_REPORT(본문 JSON) - 4600 은 예약만
241
+
242
+ _LOG = logging.getLogger("ilink.propv2")
243
+
244
+
245
+ def require_v2(version: str | None, what: str) -> None:
246
+ """상대가 Property v2 를 말하지 않으면 보내지 않고 거절한다(7 신규 오브젝트는 옛 와이어가 없다).
247
+
248
+ Args:
249
+ version (str | None): 접속 응답의 버전 문자열.
250
+ what (str): 사유에 실을 대상(``"quota"`` 등).
251
+
252
+ Raises:
253
+ ILOperationException: ``MIMQE_NOT_SUPPORTED (<what> requires ILink 7, current: <버전>)``.
254
+ """
255
+ if not peer_speaks_v2(version):
256
+ from .exception import ILOperationException
257
+ raise ILOperationException("MIMQE_NOT_SUPPORTED (%s requires ILink 7, current: %s)" % (what, version or "?"))
258
+
259
+
260
+ def request(helper, qmgr_name: str, code: int, obj: dict) -> dict:
261
+ """요청 하나를 보내고 응답 본문을 푼다. ``_IGNORED`` 가 있으면 WARNING 로그를 남긴다.
262
+
263
+ Args:
264
+ helper: 연결(``ILRequestHelper``).
265
+ qmgr_name (str): 대상 큐 관리자 이름. ``""`` 이면 서비스(ilsvc)가 직접 처리한다(쿼터 · 디스크 가드).
266
+ code (int): ``PROP_CREATE`` · ``PROP_ALTER`` · ``PROP_GET`` · ``PROP_LIST`` · ``PROP_DELETE`` 중 하나.
267
+ obj (dict): 본문(``_TYPE`` 포함).
268
+
269
+ Returns:
270
+ dict: 응답 본문. 빈 결과면 ``{}``.
271
+
272
+ Raises:
273
+ ILOperationException: 엔진이 거절했을 때(4601 의 사유 그대로). 토픽 FLUSH 와 겹친 거절은 조용히 몇 번 다시 보낸다.
274
+ PropV2Error: 응답 본문이 규칙에 맞지 않을 때.
275
+ """
276
+ from .common import ILC
277
+ from .connectivity import _FLUSH_RETRY_MAX, _flush_backoff, _is_topic_flushed
278
+ from .exception import ILOperationException
279
+ from .msg import ILMsgUtil
280
+ body = dumps(obj)
281
+ for attempt in range(_FLUSH_RETRY_MAX + 1):
282
+ msg = (ILMsgUtil.create_long_request_msg(code, body) if not qmgr_name
283
+ else ILMsgUtil.create_long_request_msg_with_qmgr(qmgr_name, code, body))
284
+ res = helper._call(msg, helper.sessionTimeoutMS)
285
+ if res.getType() == ILC.MIMQ_REPORT_MESSAGE:
286
+ exc = ILOperationException(res.getReportMsg().getArg2())
287
+ if attempt >= _FLUSH_RETRY_MAX or not _is_topic_flushed(exc):
288
+ raise exc
289
+ _flush_backoff(attempt)
290
+ continue
291
+ data = res.getDataMsg().getData()
292
+ out = parse(data) if data and bytes(data).strip(b" \t\r\n") else {}
293
+ ign = out.get("_IGNORED")
294
+ if ign:
295
+ _LOG.warning("property v2 %s %s: engine ignored %s", obj.get("_TYPE", "?"), code,
296
+ ", ".join("%s(%s)" % (e.get("KEY", "?"), e.get("WHY", "?")) for e in ign if isinstance(e, dict)))
297
+ return out
298
+ raise AssertionError("unreachable")
299
+
300
+
301
+ # ── 토픽(엔진 설계 §13-3) ─────────────────────────────────────────────────────
302
+
303
+ #: 토픽 설정 키의 종류. 옵션 문자열(``"k=v;..."``)을 본문으로 바꿀 때 쓴다 - 표에 없는 키는 STR 로 실어 보내고
304
+ #: 엔진이 모르면 ``_IGNORED unknown`` 으로 돌려준다(U4).
305
+ TOPIC_KINDS = {
306
+ "PARTITIONS": "INT", "INDEXINTERVAL": "INT",
307
+ "SEGMENTBYTES": "LONG", "SEGMENTROLLMS": "LONG", "RETENTIONMS": "LONG", "RETENTIONBYTES": "LONG",
308
+ "APPENDRUNBYTES": "LONG", "TOMBSTONERETENTIONMS": "LONG",
309
+ "MINCLEANABLERATIO": "DOUBLE", "SAFERETENTION": "BOOL", "CLEANUPPOLICY": "STR",
310
+ }
311
+ #: 읽기 전용 칸의 종류(GET / LIST 응답)
312
+ TOPIC_RO_KINDS = {
313
+ "EPOCH": "LONG", "EARLIEST": "LONG", "NEXT": "LONG", "TOTALBYTES": "LONG", "PUBLISHED": "LONG",
314
+ "DELIVERED": "LONG", "CURSORJUMPS": "LONG", "EXPIREDSUBS": "LONG", "SEGMENTCOUNT": "INT", "SUBSCRIPTIONS": "INT",
315
+ }
316
+
317
+
318
+ def topic_options(options: str) -> dict:
319
+ """옛 옵션 문자열(``"partitions=3;retentionms=86400000"``)을 본문 키로 바꾼다.
320
+
321
+ 키는 대소문자를 가리지 않고 대문자로 바꾼다(옛 엔진 ApplyOptions 와 같다). 값은 :data:`TOPIC_KINDS` 의 종류로
322
+ 바꾸고, 표에 없는 키는 문자열로 싣는다.
323
+
324
+ Args:
325
+ options (str): ``key=value`` 를 ``;`` 로 이은 문자열. 빈 칸은 건너뛴다.
326
+
327
+ Returns:
328
+ dict: 본문 키 -> 값.
329
+
330
+ Raises:
331
+ PropV2Error: ``=`` 가 없는 칸, 키 문자 규칙 위반, 값이 그 키의 종류로 읽히지 않을 때.
332
+ """
333
+ out: dict = {}
334
+ for tok in str(options or "").split(";"):
335
+ tok = tok.strip()
336
+ if not tok:
337
+ continue
338
+ eq = tok.find("=")
339
+ if eq < 0:
340
+ raise PropV2Error("topic option without '=' : %s" % tok)
341
+ key = tok[:eq].strip().upper()
342
+ val = tok[eq + 1:].strip()
343
+ if not is_valid_key(key) or key.startswith("_"):
344
+ raise PropV2Error("bad topic option key '%s'" % tok[:eq].strip())
345
+ kind = TOPIC_KINDS.get(key, "STR")
346
+ try:
347
+ if kind == "INT":
348
+ n = int(val, 10)
349
+ if not _INT_MIN <= n <= _INT_MAX:
350
+ raise ValueError(val)
351
+ out[key] = n
352
+ elif kind == "LONG":
353
+ if not _LONG_RE.match(val) or not _LONG_MIN <= int(val) <= _LONG_MAX:
354
+ raise ValueError(val)
355
+ out[key] = long_str(int(val))
356
+ elif kind == "DOUBLE":
357
+ d = float(val)
358
+ if d != d or d in (float("inf"), float("-inf")):
359
+ raise ValueError(val)
360
+ out[key] = d
361
+ elif kind == "BOOL":
362
+ if val.lower() not in ("true", "false"):
363
+ raise ValueError(val)
364
+ out[key] = val.lower() == "true"
365
+ else:
366
+ out[key] = val.upper() if key == "CLEANUPPOLICY" else val
367
+ except ValueError:
368
+ raise PropV2Error("%s must be %s : %s" % (key, kind, val)) from None
369
+ return out
370
+
371
+
372
+ def topic_list_text(obj: dict) -> str:
373
+ """LIST 응답을 옛 목록 줄(``name|earliest|next|epoch|partitions`` + ``\\n``)로 다시 만든다.
374
+
375
+ Args:
376
+ obj (dict): LIST 응답 본문(``_ITEMS``).
377
+
378
+ Returns:
379
+ str: 토픽당 한 줄(엔진 옛 GetTopicDisplayList 와 같은 모양). 토픽이 없으면 빈 문자열.
380
+ """
381
+ out = []
382
+ for e in obj.get("_ITEMS", []) or []:
383
+ if not isinstance(e, dict):
384
+ raise PropV2Error("_ITEMS must be ARRAY of OBJ")
385
+ out.append("%s|%d|%d|%d|%d\n" % (take(e, "NAME", "STR") or "", take(e, "EARLIEST", "LONG") or 0,
386
+ take(e, "NEXT", "LONG") or 0, take(e, "EPOCH", "LONG") or 0,
387
+ take(e, "PARTITIONS", "INT") or 0))
388
+ return "".join(out)
389
+
390
+
391
+ def topic_names(obj: dict) -> list:
392
+ """LIST 응답에서 이름만 모은다.
393
+
394
+ Returns:
395
+ list[str]: 토픽 이름들(응답 순서).
396
+ """
397
+ return [take(e, "NAME", "STR") or "" for e in obj.get("_ITEMS", []) or [] if isinstance(e, dict)]
398
+
399
+
400
+ def topic_detail_text(obj: dict) -> str:
401
+ """GET 응답을 사람이 읽는 평문으로 만든다(형식은 고정하지 않는다).
402
+
403
+ Returns:
404
+ str: ``KEY : 값`` 줄들과 파티션마다 ``PART n ...`` 줄.
405
+ """
406
+ lines = []
407
+ for k, v in obj.items():
408
+ if k.startswith("_") or k == "PARTS":
409
+ continue
410
+ lines.append("%s : %s" % (k, ("true" if v else "false") if isinstance(v, bool) else v))
411
+ for p in obj.get("PARTS", []) or []:
412
+ if isinstance(p, dict):
413
+ lines.append("PART %s : earliest=%s next=%s bytes=%s leader=%s epoch=%s isr=%s" % (
414
+ p.get("PARTITION", "?"), p.get("EARLIEST", "?"), p.get("NEXT", "?"), p.get("BYTES", "?"),
415
+ p.get("LEADER", "?"), p.get("EPOCH", "?"), p.get("ISR", "")))
416
+ return "".join(x + "\n" for x in lines)
417
+
418
+
419
+ def peer_speaks_v2(version: str | None) -> bool:
420
+ """상대(접속 응답 arg2 의 버전 문자열)가 Property v2 를 말하는가.
421
+
422
+ 앞뒤 공백 · 탭을 뺀 뒤 첫 '.'(없으면 끝)까지가 비지 않은 숫자열(6자리 이하)이고 그 값이 7 이상이면 참.
423
+ revision · 브랜드는 보지 않는다. 비었거나 못 읽으면 거짓(기존 방식). split 을 쓰지 않는다 - ".7.0.1" 은 거짓이다.
424
+
425
+ Returns:
426
+ bool: Property v2 를 말하면 True.
427
+ """
428
+ if not version:
429
+ return False
430
+ s = version.lstrip(" \t")
431
+ if not s:
432
+ return False
433
+ i = s.find(".")
434
+ major = (s if i < 0 else s[:i]).rstrip(" \t")
435
+ if not major or len(major) > 6:
436
+ return False
437
+ for c in major:
438
+ if not ("0" <= c <= "9"):
439
+ return False
440
+ return int(major) >= 7