DefStream 0.1.3.4__tar.gz → 0.1.3.7__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 (85) hide show
  1. defstream-0.1.3.7/DefStream.egg-info/PKG-INFO +538 -0
  2. {defstream-0.1.3.4 → defstream-0.1.3.7}/DefStream.egg-info/SOURCES.txt +4 -1
  3. defstream-0.1.3.7/PKG-INFO +538 -0
  4. defstream-0.1.3.7/README.en.md +518 -0
  5. defstream-0.1.3.7/README.md +505 -0
  6. defstream-0.1.3.7/src/Worker/Startup.py +135 -0
  7. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/Worker.py +2 -0
  8. defstream-0.1.3.7/test/test_worker_startup.py +390 -0
  9. defstream-0.1.3.4/DefStream.egg-info/PKG-INFO +0 -1258
  10. defstream-0.1.3.4/PKG-INFO +0 -1258
  11. defstream-0.1.3.4/README.md +0 -1225
  12. {defstream-0.1.3.4 → defstream-0.1.3.7}/.github/workflows/ci.yml +0 -0
  13. {defstream-0.1.3.4 → defstream-0.1.3.7}/.gitignore +0 -0
  14. {defstream-0.1.3.4 → defstream-0.1.3.7}/DefStream.egg-info/dependency_links.txt +0 -0
  15. {defstream-0.1.3.4 → defstream-0.1.3.7}/DefStream.egg-info/requires.txt +0 -0
  16. {defstream-0.1.3.4 → defstream-0.1.3.7}/DefStream.egg-info/top_level.txt +0 -0
  17. {defstream-0.1.3.4 → defstream-0.1.3.7}/LICENSE +0 -0
  18. {defstream-0.1.3.4 → defstream-0.1.3.7}/sample/Broker/Sample.py +0 -0
  19. {defstream-0.1.3.4 → defstream-0.1.3.7}/sample/Worker/RabbitMQ/Client.py +0 -0
  20. {defstream-0.1.3.4 → defstream-0.1.3.7}/sample/Worker/RabbitMQ/Tasks.py +0 -0
  21. {defstream-0.1.3.4 → defstream-0.1.3.7}/sample/Worker/RabbitMQ/Worker.py +0 -0
  22. {defstream-0.1.3.4 → defstream-0.1.3.7}/sample/Worker/Redis/Sample.py +0 -0
  23. {defstream-0.1.3.4 → defstream-0.1.3.7}/sample/Worker/Sample.py +0 -0
  24. {defstream-0.1.3.4 → defstream-0.1.3.7}/setup.cfg +0 -0
  25. {defstream-0.1.3.4 → defstream-0.1.3.7}/setup.py +0 -0
  26. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Configuration.py +0 -0
  27. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Connection.py +0 -0
  28. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Consumer.py +0 -0
  29. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Errors.py +0 -0
  30. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Event.py +0 -0
  31. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Implements/__init__.py +0 -0
  32. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Policies.py +0 -0
  33. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/Protocol.py +0 -0
  34. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Broker/__init__.py +0 -0
  35. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Descriptor/DataObject.py +0 -0
  36. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Descriptor/Types.py +0 -0
  37. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Descriptor/Value.py +0 -0
  38. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Descriptor/__init__.py +0 -0
  39. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extension.py +0 -0
  40. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/Console.html +0 -0
  41. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/Console.py +0 -0
  42. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/Dashboard.html +0 -0
  43. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/Dashboard.py +0 -0
  44. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/HealthCheck.py +0 -0
  45. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/Metrics.py +0 -0
  46. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/Specifications.py +0 -0
  47. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Extensions/__init__.py +0 -0
  48. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Inspector.py +0 -0
  49. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/Response.py +0 -0
  50. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Inspector/__init__.py +0 -0
  51. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Pattern.py +0 -0
  52. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Array.py +0 -0
  53. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Common.py +0 -0
  54. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Compare.py +0 -0
  55. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Condition.py +0 -0
  56. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/DataObject.py +0 -0
  57. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/DateTime.py +0 -0
  58. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Number.py +0 -0
  59. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Object.py +0 -0
  60. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Size.py +0 -0
  61. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/String.py +0 -0
  62. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/Type.py +0 -0
  63. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Patterns/__init__.py +0 -0
  64. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Validate.py +0 -0
  65. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/Validator.py +0 -0
  66. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Validator/__init__.py +0 -0
  67. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/Executor.py +0 -0
  68. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/Message.py +0 -0
  69. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/Pool.py +0 -0
  70. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/Run.py +0 -0
  71. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/Specification.py +0 -0
  72. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/Worker/__init__.py +0 -0
  73. {defstream-0.1.3.4 → defstream-0.1.3.7}/src/__init__.py +0 -0
  74. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_dashboard.py +0 -0
  75. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_dashboard_ui.cjs +0 -0
  76. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_inspector.py +0 -0
  77. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_inspector_console.py +0 -0
  78. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_lifecycle_hooks.py +0 -0
  79. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_pool_callbacks.py +0 -0
  80. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_process_signals.py +0 -0
  81. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_protocol_contracts.py +0 -0
  82. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_runner_hooks.py +0 -0
  83. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_specifications.py +0 -0
  84. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_task_messages.py +0 -0
  85. {defstream-0.1.3.4 → defstream-0.1.3.7}/test/test_worker.py +0 -0
@@ -0,0 +1,538 @@
1
+ Metadata-Version: 2.4
2
+ Name: DefStream
3
+ Version: 0.1.3.7
4
+ Summary: Synchronous message processing with pluggable broker adapters
5
+ Home-page: https://github.com/yong5eon/DefStream
6
+ Author: Heo Yongseon
7
+ Project-URL: Source, https://github.com/yong5eon/DefStream
8
+ Project-URL: Issues, https://github.com/yong5eon/DefStream/issues
9
+ Project-URL: Releases, https://github.com/yong5eon/DefStream/releases
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: rabbitmq
20
+ Requires-Dist: DefStream-RabbitMQ>=0.1.0; extra == "rabbitmq"
21
+ Provides-Extra: redis
22
+ Requires-Dist: DefStream-Redis>=0.1.0; extra == "redis"
23
+ Dynamic: author
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: license-file
29
+ Dynamic: project-url
30
+ Dynamic: provides-extra
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ # DefStream
35
+
36
+ **한국어** | [English](./README.en.md)
37
+
38
+ **Just def.**<br>
39
+ **Let workers do the work.**<br>
40
+ **You don't need to await this.**
41
+
42
+ DefStream은 메시지 브로커에서 받은 작업을 일반 Python 함수처럼 작성하고,
43
+ 스레드 또는 프로세스 풀에서 실행하는 동기식 작업 처리 라이브러리입니다.
44
+ `async` / `await` 없이 작업 실행, 처리 확인(ACK), 실패 정책, 상태 모니터링을 구성할 수 있습니다.
45
+
46
+ - **작업 정의**: `Run` 클래스에 구독 대상과 처리 함수를 선언합니다.
47
+ - **병렬 실행**: `ThreadPool` 또는 `ProcessPool`을 선택합니다.
48
+ - **브로커 선택**: RabbitMQ와 Redis Streams 어댑터를 별도로 설치합니다.
49
+ - **요청·응답**: RabbitMQ에서 작업의 반환값을 RPC 응답으로 받을 수 있습니다.
50
+ - **운영 도구**: HTTP 상태 확인, Prometheus 지표, 대시보드, 작업 콘솔을 선택적으로 제공합니다.
51
+ - **입출력 계약**: 스키마와 런타임 검증을 정의하고 AsyncAPI 문서로 노출합니다.
52
+
53
+ ## 목차
54
+
55
+ - [설치](#설치)
56
+ - [빠르게 시작하기](#빠르게-시작하기)
57
+ - [작업과 메시지 처리](#작업과-메시지-처리)
58
+ - [실행 풀과 종료](#실행-풀과-종료)
59
+ - [생명주기 훅](#생명주기-훅)
60
+ - [요청·응답 계약과 검증](#요청응답-계약과-검증)
61
+ - [상태 확인과 작업 콘솔](#상태-확인과-작업-콘솔)
62
+ - [브로커 선택과 운영 주의사항](#브로커-선택과-운영-주의사항)
63
+ - [예제와 라이선스](#예제와-라이선스)
64
+
65
+ ## 설치
66
+
67
+ Python **3.9 이상**이 필요합니다. 실제 메시지 처리를 위해 사용할 브로커 서버도 준비하세요.
68
+
69
+ ```shell
70
+ python -m pip install "DefStream[RabbitMQ]"
71
+ ```
72
+
73
+ Redis를 사용하거나 두 어댑터를 모두 설치하려면 다음과 같이 선택합니다.
74
+
75
+ ```shell
76
+ python -m pip install "DefStream[Redis]"
77
+ python -m pip install "DefStream[RabbitMQ,Redis]"
78
+ ```
79
+
80
+ 코어만 필요하면 `python -m pip install DefStream`을 사용합니다.
81
+ 코어에는 브로커 클라이언트가 포함되지 않습니다. 어댑터를 직접 설치해도 코어가 함께 설치됩니다.
82
+
83
+ | 배포 패키지 | Python import 경로 | 사용 안내 |
84
+ | --- | --- | --- |
85
+ | `DefStream` | `DefStream` | 이 문서 |
86
+ | `DefStream-RabbitMQ` | `DefStream.Broker.Implements.RabbitMQ` | [RabbitMQ 안내](https://github.com/yong5eon/DefStream-RabbitMQ#readme) |
87
+ | `DefStream-Redis` | `DefStream.Broker.Implements.Redis` | [Redis 안내](https://github.com/yong5eon/DefStream-Redis#readme) |
88
+
89
+ ## 빠르게 시작하기
90
+
91
+ 로컬 RabbitMQ가 `127.0.0.1:5672`에서 실행 중이라고 가정합니다.
92
+ 아래 세 파일을 같은 디렉터리에 저장하세요. `guest` 계정은 로컬 실행 예제이며,
93
+ 운영에서는 별도 계정과 환경변수·비밀 저장소를 사용하세요.
94
+
95
+ ### 1. 작업 정의: `tasks.py`
96
+
97
+ ```python
98
+ from DefStream.Broker.Implements.RabbitMQ import Reject, Subscription
99
+ from DefStream.Worker import Run
100
+
101
+
102
+ class Add(
103
+ Run,
104
+ parameters=Subscription(
105
+ queue='defstream.example.add',
106
+ qos=4,
107
+ error='defstream.example.add.error',
108
+ ),
109
+ on_error=Reject(),
110
+ ):
111
+ def run(
112
+ self,
113
+ a: int,
114
+ b: int,
115
+ ):
116
+ result = a + b
117
+ print(result)
118
+ return result
119
+ ```
120
+
121
+ `parameters`는 작업이 소비할 큐를 지정합니다. 메시지 본문 `{'a': 10, 'b': 20}`은
122
+ `run(a=10, b=20)`으로 전달됩니다. 이 예제는 실패 시 재시도 대신 거부하며,
123
+ 거부된 메시지를 받을 오류 큐도 선언합니다.
124
+
125
+ ### 2. Worker 실행: `worker.py`
126
+
127
+ ```python
128
+ from signal import SIGINT, signal
129
+
130
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
131
+ from DefStream.Worker import ThreadPool, Worker, WorkerSetup, declare
132
+ from tasks import Add
133
+
134
+
135
+ class Prepare(WorkerSetup):
136
+ def __call__(
137
+ self,
138
+ worker: Worker,
139
+ ) -> None:
140
+ for task in worker.tasks:
141
+ declare(
142
+ task=task,
143
+ broker=worker.broker,
144
+ )
145
+
146
+
147
+ if __name__ == '__main__':
148
+ worker = Worker(
149
+ Connection(Configuration(
150
+ host='127.0.0.1',
151
+ port=5672,
152
+ username='guest',
153
+ password='guest',
154
+ )),
155
+ ThreadPool(4),
156
+ setups=Prepare(),
157
+ )
158
+ worker.add(Add)
159
+ signal(
160
+ SIGINT,
161
+ lambda *_: worker.stop(),
162
+ )
163
+ worker.run()
164
+ ```
165
+
166
+ ```shell
167
+ python worker.py
168
+ ```
169
+
170
+ Worker가 브로커 연결을 열고, `Prepare`가 큐와 오류 큐를 선언한 뒤 구독을 시작합니다.
171
+ `Run`을 정의하는 것만으로 등록되지는 않으며 `worker.add()`가 필요합니다.
172
+
173
+ ### 3. 메시지 보내기: `client.py`
174
+
175
+ Worker가 실행 중인 상태에서 다른 터미널에서 실행하세요.
176
+
177
+ ```python
178
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
179
+ from DefStream.Worker import request, send
180
+ from tasks import Add
181
+
182
+
183
+ connection = Connection(Configuration(
184
+ host='127.0.0.1',
185
+ port=5672,
186
+ username='guest',
187
+ password='guest',
188
+ ))
189
+ try:
190
+ connection.connect()
191
+ message_id = send(
192
+ {'a': 10, 'b': 20},
193
+ task=Add,
194
+ broker=connection,
195
+ )
196
+ print(message_id)
197
+ result = request(
198
+ {'a': 10, 'b': 20},
199
+ task=Add,
200
+ broker=connection,
201
+ timeout=5,
202
+ )
203
+ print(result)
204
+ finally:
205
+ connection.close()
206
+ ```
207
+
208
+ ```shell
209
+ python client.py
210
+ ```
211
+
212
+ `send()`와 `request()`는 **각각 별도의 작업을 발행**합니다. 위 예제는 덧셈을 두 번 실행하며,
213
+ `request()`의 결과는 `30`입니다. `request()`의 제한 시간은 초 단위이고,
214
+ 시간이 초과되어도 이미 전달된 작업은 취소되지 않습니다.
215
+
216
+ ## 작업과 메시지 처리
217
+
218
+ ### 작업을 대상으로 하는 API
219
+
220
+ | 함수 | 역할 | 연결 관리 |
221
+ | --- | --- | --- |
222
+ | `declare(task=..., broker=...)` | 작업의 큐·구독 리소스 선언 | 호출자가 연결·종료 |
223
+ | `send(body, task=..., broker=...)` | 작업 목적지로 단방향 메시지 발행 | 호출자가 연결·종료 |
224
+ | `request(body, task=..., broker=..., timeout=5)` | 작업 결과를 동기 RPC로 요청 | 호출자가 연결·종료; RabbitMQ만 지원 |
225
+
226
+ 세 함수는 `DefStream.Worker`에서 가져옵니다. 전달된 연결을 자동으로 열거나 닫지 않으며,
227
+ `send()`와 `request()`는 목적지를 자동 선언하지 않습니다.
228
+ 수신 Worker 또는 별도의 준비 단계에서 먼저 `declare()`를 호출하세요.
229
+ 하나의 연결을 같은 소유 스레드에서 여러 발행에 재사용할 수 있습니다.
230
+
231
+ 작업 본문은 **문자열 키를 가진 매핑**이어야 하며, 반환값은 어댑터가 직렬화할 수 있어야 합니다.
232
+ 하나의 Worker에서는 같은 큐에 여러 작업을 중복 등록할 수 없습니다.
233
+ 메시지 유형을 보고 여러 `Run` 중 하나를 고르는 방식이 아니라 큐별로 작업을 선택합니다.
234
+
235
+ ### 성공, 실패와 재시도
236
+
237
+ - 작업 실행과 완료 훅이 성공한 뒤 ACK합니다. RabbitMQ RPC는 응답 발행도 ACK보다 먼저 수행합니다.
238
+ - 실패 시 `on_error` 정책을 적용합니다. 기본값은 `Requeue()`이며, `Reject()`를 명시할 수 있습니다.
239
+ - `Requeue`와 `Reject`는 공통으로 `DefStream.Broker.Policies`에서 가져올 수 있습니다.
240
+ - `Reject()`의 실제 효과는 브로커에 따라 다릅니다. RabbitMQ는 설정된 DLQ로 보내거나 폐기하며,
241
+ Redis는 해당 그룹에서 ACK하고 스트림 이력은 유지합니다.
242
+ - 재시도 횟수의 자동 제한은 없습니다. 영구적인 오류가 무한 재전달되지 않도록 정책을 선택하세요.
243
+ - 전송과 ACK는 하나의 트랜잭션이 아닙니다. **중복 실행에 안전한 멱등 작업**을 작성해야 합니다.
244
+
245
+ 브로커 리소스를 직접 사용할 때의 공통 메서드는 다음과 같습니다.
246
+ 모든 리소스가 모든 메서드를 지원하는 것은 아닙니다.
247
+
248
+ | 메서드 | 의미 | 제한 시간 단위 |
249
+ | --- | --- | --- |
250
+ | `send(body)` | 메시지 발행 | 어댑터별 설정 |
251
+ | `call(body, timeout=5)` | 동기 RPC | 초 |
252
+ | `pop(timeout=1000)` | 한 메시지 수신 | 밀리초 |
253
+ | `read(offset=0, timeout=1000)` | 스트림 이력·새 메시지 읽기 | 밀리초 |
254
+
255
+ `pop()`과 `read()`에서 `None` 또는 `0`은 무기한 대기입니다.
256
+ 직접 수신한 처리용 이벤트는 `ack()`, `nack()`, `reject()`로 정산하세요.
257
+ Redis 이력 읽기는 그룹 처리와 다르며 ACK 대상이 아닙니다.
258
+ RabbitMQ 스트림 읽기는 이터레이터가 다음 단계로 진행될 때 자동 ACK합니다.
259
+
260
+ ## 실행 풀과 종료
261
+
262
+ | 풀 | 권장 용도 | 주의사항 |
263
+ | --- | --- | --- |
264
+ | `ThreadPool(4)` | I/O 대기 중심 작업 | 실행 스레드 사이에 공유하는 상태 보호 |
265
+ | `ProcessPool(4)` | CPU 연산 중심 작업 | 클래스·입력·결과·훅은 pickle 가능해야 함 |
266
+
267
+ 두 풀은 `DefStream.Worker`에서 가져옵니다. 크기를 생략하면 CPU 수를 사용하며,
268
+ 공유 풀의 용량은 제출되어 처리 중인 작업을 포함합니다. 무제한 대기열은 없습니다.
269
+ 풀이 가득 차면 Worker는 메시지를 재전달하도록 NACK합니다.
270
+ RabbitMQ의 작업별 `Subscription.qos`는 미확인 메시지 수 제한이며 풀 크기와는 다릅니다.
271
+
272
+ `ProcessPool`을 사용할 때는 클래스와 훅을 import 가능한 모듈 최상위에 정의하고,
273
+ Worker 생성·실행을 `if __name__ == '__main__':` 안에 두세요.
274
+ 브로커 연결이나 이벤트 객체를 자식 프로세스로 넘기지 마세요.
275
+
276
+ `worker.stop()`은 새 작업 수신을 멈추고 실행 중인 작업과 메시지 정산을 기다린 후 자원을 닫습니다.
277
+ 현재 강제 종료나 종료 대기 시간 제한은 없습니다. 위 예제처럼 애플리케이션에서
278
+ `SIGINT`를 연결하면 Ctrl+C로 정상 종료할 수 있습니다.
279
+ Worker와 주입한 연결·풀은 한 번의 실행 전용이며 다른 Worker와 공유하거나 재시작하지 않습니다.
280
+
281
+ ## 생명주기 훅
282
+
283
+ 훅 인터페이스는 모두 `DefStream.Worker`에서 가져옵니다.
284
+
285
+ | 범위 | 훅 | 전달값 및 실행 위치 |
286
+ | --- | --- | --- |
287
+ | 작업 | `RunSetup`, `RunComplete`, `RunError` | `Context`; `Context`와 결과; `Context`와 예외. 풀 안에서 실행 |
288
+ | 풀 | `PoolSetup`, `PoolCleanup` | 인자 없음. 각 실행 스레드·프로세스 안에서 실행 |
289
+ | Worker | `WorkerSetup`, `WorkerCleanup` | Worker 인스턴스. 부모의 연결 소유 스레드에서 실행 |
290
+
291
+ `Run` 클래스에는 `setups=`, `completes=`, `errors=`를 지정합니다.
292
+ Worker와 풀 생성자에는 `setups=`, `cleanups=`를 지정합니다.
293
+ 각 옵션은 하나의 훅 또는 순서 있는 훅 시퀀스를 받습니다.
294
+ `Context.args`, `Context.kwargs`에서 호출 인자를 확인할 수 있습니다.
295
+
296
+ 풀의 각 실행 환경은 첫 작업 때 생성되며 초기화·정리 훅을 한 번씩 실행합니다.
297
+ DB 연결처럼 실행 환경에 종속된 자원은 여기서 생성·종료하세요.
298
+ Worker 초기화 훅은 브로커 연결 후 구독 전에 실행되고,
299
+ 정리 훅은 풀 종료 후 소비자·브로커가 닫히기 전에 실행됩니다.
300
+ 초기화 도중 실패할 수도 있으므로 정리 훅은 부분 초기화 상태를 고려해야 합니다.
301
+ 프로세스 강제 종료나 충돌 시 정리 훅 실행은 보장되지 않습니다.
302
+
303
+ `RunError`는 오류 처리용 코드이고 `on_error`는 메시지 정산 정책입니다.
304
+ 오류 훅을 실행했다고 작업이 성공으로 바뀌지는 않습니다.
305
+ 완료 훅에서 다음 작업을 발행할 때는 그 훅의 스레드·프로세스에서 별도 연결을 열고 닫으세요.
306
+ 일부 발행 후 실패하더라도 이미 보낸 메시지는 롤백되지 않습니다.
307
+
308
+ ## 요청·응답 계약과 검증
309
+
310
+ `Descriptor`는 **데이터의 구조를 설명**하고, `Validator`는 **실제 값을 검증·변환**합니다.
311
+ 두 도구를 `Specification`에 연결하면 작업의 문서와 실행 시 검증을 함께 구성할 수 있습니다.
312
+ 다음 예제들은 위에서부터 같은 파일에 이어서 작성하세요.
313
+
314
+ ### 1. Descriptor로 스키마 작성
315
+
316
+ ```python
317
+ from DefStream.Descriptor import DumpSchema, Integer, Object, Properties, Schema
318
+
319
+
320
+ request_schema = Object(
321
+ description='Addition request',
322
+ properties=Properties(
323
+ a=Integer('First operand'),
324
+ b=Integer('Second operand'),
325
+ ),
326
+ )
327
+ response_schema = Integer('Sum')
328
+
329
+ print(DumpSchema(Schema(
330
+ 'AddRequest',
331
+ request_schema,
332
+ )))
333
+ ```
334
+
335
+ `Object`는 객체, `Properties`는 필드, `Integer`는 정수를 설명합니다.
336
+ `Integer` 등의 필드는 기본적으로 필수이며 `required=False`로 선택 필드를 표시할 수 있습니다.
337
+ `String`, `Number`, `Boolean`, `Array` 등으로 다른 타입과 중첩 구조도 표현합니다.
338
+ `DumpSchema()`에는 `Schema`로 감싼 값을 전달하여 일반 JSON Schema 딕셔너리를 얻습니다.
339
+
340
+ 이 스키마는 `a`, `b`를 정수 필수 필드로 문서화하지만 **메시지를 자동 검증하지는 않습니다.**
341
+ 스키마의 필수 여부·기본값·범위를 실제 실행에서도 적용하려면 대응하는 검증 규칙이 필요합니다.
342
+
343
+ ### 2. Validator로 값 검증
344
+
345
+ ```python
346
+ from DefStream.Validator import Validator
347
+ from DefStream.Validator.Patterns import IsInteger, IsObject
348
+ from DefStream.Validator.Patterns.Object import IsMappingOf, IsRequiredIn
349
+
350
+
351
+ request_validator = Validator(
352
+ IsObject(
353
+ IsRequiredIn(
354
+ 'a',
355
+ 'b',
356
+ ),
357
+ IsMappingOf({
358
+ 'a': IsInteger(),
359
+ 'b': IsInteger(),
360
+ }),
361
+ ),
362
+ )
363
+ response_validator = Validator(IsInteger())
364
+
365
+ print(request_validator({'a': 10, 'b': 20}))
366
+ ```
367
+
368
+ - `Validator`는 패턴을 지정한 순서대로 적용하고 마지막 값을 반환합니다.
369
+ - `IsObject`는 `dict` 타입을 확인하고, 내부 패턴으로 필수 키와 각 필드의 타입을 검사합니다.
370
+ - `IsRequiredIn`은 키 누락 시 `ValueError`, `IsInteger`는 잘못된 타입에 `TypeError`를 발생시킵니다.
371
+ 문자열 `'10'`이나 불리언 `True`를 정수로 인정하지 않습니다.
372
+ - `IsMappingOf`는 지정한 필드를 검증·변환하며 딕셔너리를 직접 갱신할 수 있습니다.
373
+ 원본 보존이 필요하면 복사본을 전달하세요. 지정하지 않은 추가 키를 제거하거나 거부하지는 않습니다.
374
+ 추가 키가 작업 함수의 인자와 맞지 않으면 호출이 실패하므로 필요한 제한을 별도로 작성하세요.
375
+
376
+ ### 3. 검증과 변환 조합
377
+
378
+ 숫자 문자열을 허용하려면 `IsInteger` 대신 명시적인 변환 패턴을 사용합니다.
379
+ 아래는 문자열을 정수로 변환한 후 0 이상인지 검사하는 독립적인 예제입니다.
380
+
381
+ ```python
382
+ from DefStream.Validator import Validator
383
+ from DefStream.Validator.Patterns import IsGreaterEqualTo, ToInteger
384
+
385
+
386
+ non_negative_integer = Validator(
387
+ ToInteger(),
388
+ IsGreaterEqualTo(0),
389
+ )
390
+
391
+ print(non_negative_integer('10'))
392
+ ```
393
+
394
+ 결과는 문자열이 아닌 정수 `10`입니다. 변환할 수 없는 문자열이나 음수는 `ValueError`가 발생합니다.
395
+ `ToInteger`는 Python 숫자 리터럴 형식의 문자열을 처리하며, 엄격한 타입 검사와 동작이 다릅니다.
396
+ 이 변환기는 위의 엄격한 요청 검증기를 바꾸지 않습니다. 필드에 적용하려면
397
+ `IsMappingOf`의 해당 값에 `non_negative_integer`를 넣으세요.
398
+
399
+ ### 4. 작업의 요청·응답에 연결
400
+
401
+ 앞에서 만든 스키마와 검증기를 사용하여 기존 `Add`를 다음처럼 대체합니다.
402
+ 이 클래스를 Worker에 등록하면 요청은 `run()` 전에, 반환값은 완료 훅 전에 검증합니다.
403
+
404
+ ```python
405
+ from DefStream.Broker.Implements.RabbitMQ import Reject, Subscription
406
+ from DefStream.Worker import Run, Specification
407
+
408
+
409
+ class Add(
410
+ Run,
411
+ parameters=Subscription(queue='defstream.example.validated-add'),
412
+ on_error=Reject(),
413
+ request=Specification(
414
+ schema=request_schema,
415
+ validator=request_validator,
416
+ example={'a': 10, 'b': 20},
417
+ description='Add two integers',
418
+ ),
419
+ response=Specification(
420
+ schema=response_schema,
421
+ validator=response_validator,
422
+ example=30,
423
+ ),
424
+ ):
425
+ def run(
426
+ self,
427
+ a: int,
428
+ b: int,
429
+ ):
430
+ return a + b
431
+ ```
432
+
433
+ - `schema`는 문서용입니다. JSON Schema 매핑, Descriptor 값·조건, dataclass 타입 등을 받지만,
434
+ 그 자체로 런타임 검증이나 모델 생성을 수행하지 않습니다.
435
+ - `validator`는 호출 가능한 검증·변환 함수이며 반환한 값을 실제 실행에 사용합니다.
436
+ 요청 검증 결과도 문자열 키를 가진 매핑이어야 합니다.
437
+ - 일반 JSON Schema 딕셔너리와 직접 작성한 검증 함수도 각각 `schema`, `validator`에 전달할 수 있습니다.
438
+ - `example`은 문서용 값이며 자동 실행·검증하지 않습니다.
439
+ - `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
440
+ - 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
441
+
442
+ 검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
443
+ 예제의 `Reject()`는 잘못된 입력을 무한 재시도하지 않게 합니다. DLQ에 보관하려면
444
+ `Subscription(error=...)`도 지정하고 `declare()`로 해당 큐를 준비하세요.
445
+ 스키마·예제는 Inspector의 AsyncAPI와 콘솔에 표시되며,
446
+ 사용자 정의 Validator 패턴을 자동으로 JSON Schema로 역변환하지는 않습니다.
447
+
448
+ ## 상태 확인과 작업 콘솔
449
+
450
+ 선택 기능인 Inspector는 Python 표준 라이브러리 HTTP 서버를 사용합니다.
451
+ 다음 코드를 Worker 실행 전에 구성하고 `Worker(..., inspector=inspector)`로 전달하세요.
452
+ 앞의 `worker.py`에서 `Connection`, `Configuration` import를 그대로 사용합니다.
453
+
454
+ ```python
455
+ from DefStream.Inspector import Inspector
456
+ from DefStream.Inspector.Extensions import (
457
+ Console,
458
+ Dashboard,
459
+ HealthCheck,
460
+ Metrics,
461
+ Specifications,
462
+ )
463
+
464
+
465
+ def create_connection():
466
+ return Connection(Configuration(
467
+ host='127.0.0.1',
468
+ port=5672,
469
+ username='guest',
470
+ password='guest',
471
+ ))
472
+
473
+
474
+ inspector = Inspector(
475
+ host='127.0.0.1',
476
+ port=9999,
477
+ exts=(
478
+ HealthCheck('/status'),
479
+ Metrics('/metrics'),
480
+ Dashboard('/dashboard'),
481
+ Specifications('/asyncapi.json'),
482
+ Console(
483
+ '/console',
484
+ broker=create_connection,
485
+ title='DefStream',
486
+ ),
487
+ ),
488
+ )
489
+ ```
490
+
491
+ | 경로 | 용도 |
492
+ | --- | --- |
493
+ | `/status` | 구독 준비와 소비자 루프 응답성을 검사. 정상 200, 시작·종료·응답 불가 상태 503 |
494
+ | `/metrics` | `defstream_` 접두사의 Prometheus 지표 |
495
+ | `/dashboard` | 처리 성공·실패·처리량·소요 시간. `Metrics` 하나와 함께 등록 |
496
+ | `/asyncapi.json` | 등록 작업의 요청·응답 계약을 AsyncAPI JSON으로 제공 |
497
+ | `/console` | 작업 목록·스키마 조회, JSON 메시지 발행, 지원 브로커의 RPC 실행 |
498
+
499
+ 기본으로 등록되는 경로는 없습니다. 지표는 한 Worker 기준이며 재시작 시 초기화됩니다.
500
+ 처리 횟수는 재시도와 훅을 포함한 실행 시도 기준이지 고유 메시지 수나 ACK 수가 아닙니다.
501
+ 대시보드는 최근 1초·60초 구간을 제공하지만 장기 지표 저장소를 대신하지 않습니다.
502
+ 응답 계약을 선언해도 Redis에 RPC 기능이 생기지는 않습니다.
503
+
504
+ **접근을 제한하세요.** 위 콘솔 예제는 인증 없이 실제 작업을 발행합니다.
505
+ 인증이 필요하면 `Console`에 `token=`으로 무작위 16자 이상 토큰을 전달하고,
506
+ 브라우저에서 입력하세요. 토큰은 환경변수나 비밀 저장소에서 읽으세요.
507
+ 다른 기본 엔드포인트는 인증되지 않으므로 신뢰할 수 있는 관리망이나 루프백에만 바인딩하고,
508
+ 원격 접근은 HTTPS 역방향 프록시 및 별도 접근 제어를 사용하세요.
509
+ Inspector 자체는 TLS를 제공하지 않으며 공개 웹 서버가 아닙니다.
510
+ 콘솔의 연결 팩터리는 호출마다 새 연결을 반환해야 하고 Worker 연결을 공유하면 안 됩니다.
511
+
512
+ ## 브로커 선택과 운영 주의사항
513
+
514
+ | 기능 | RabbitMQ | Redis Streams |
515
+ | --- | --- | --- |
516
+ | 큐 송수신 | 지원 | 스트림과 소비자 그룹 사용 |
517
+ | Exchange 라우팅 | 지원 | 미지원 |
518
+ | 스트림 이력 읽기 | 지원 | 지원 |
519
+ | 작업 RPC | 지원 | 미지원 |
520
+ | 오류 메시지 목적지 | 명시적으로 선언한 DLQ | 기본 제공하지 않음 |
521
+
522
+ - RabbitMQ 연결과 리소스는 소유 스레드에서만 사용하세요. 소비자 연결을 작업 스레드와 공유하지 마세요.
523
+ - Redis는 **6.2 이상**이 필요합니다. 같은 소비자 그룹은 메시지를 나눠 받고, 다른 그룹은 각각 받습니다.
524
+ - Redis 재큐잉은 복사본을 추가하고 원본을 ACK하므로 새 메시지는 다른 그룹에도 보입니다.
525
+ - Redis는 기본 60초 유휴 pending 메시지를 재확보합니다. 긴 작업에서는 다른 소비자와 중복 실행될 수 있습니다.
526
+ 직접 소비자를 만들 때 `reclaim_idle`을 조정하거나 `None`으로 끌 수 있으며, 기본 Worker는 기본값을 사용합니다.
527
+ - Redis ACK·거부는 이력을 삭제하지 않습니다. 보존 정책은 외부에서 관리하고 pending 메시지 삭제를 피하세요.
528
+ - 네트워크 오류의 자동 재연결이나 exactly-once 실행을 가정하지 마세요.
529
+ 애플리케이션에서 재시작 전략, 멱등성, 재시도·오류 큐 정책을 설계하세요.
530
+
531
+ ## 예제와 라이선스
532
+
533
+ - [브로커 없이 실행하는 Worker 예제](./sample/Worker/Sample.py)
534
+ - [RabbitMQ Worker·RPC 클라이언트 예제](./sample/Worker/RabbitMQ)
535
+ - [Redis Worker 예제](./sample/Worker/Redis/Sample.py)
536
+ - [릴리즈와 변경 내역](https://github.com/yong5eon/DefStream/releases)
537
+
538
+ GNU GPL v3 라이선스를 사용합니다. 자세한 조건은 [LICENSE](./LICENSE)를 확인하세요.
@@ -1,5 +1,6 @@
1
1
  .gitignore
2
2
  LICENSE
3
+ README.en.md
3
4
  README.md
4
5
  setup.py
5
6
  .github/workflows/ci.yml
@@ -61,6 +62,7 @@ src/Worker/Message.py
61
62
  src/Worker/Pool.py
62
63
  src/Worker/Run.py
63
64
  src/Worker/Specification.py
65
+ src/Worker/Startup.py
64
66
  src/Worker/Worker.py
65
67
  src/Worker/__init__.py
66
68
  test/test_dashboard.py
@@ -74,4 +76,5 @@ test/test_protocol_contracts.py
74
76
  test/test_runner_hooks.py
75
77
  test/test_specifications.py
76
78
  test/test_task_messages.py
77
- test/test_worker.py
79
+ test/test_worker.py
80
+ test/test_worker_startup.py