DefStream-RabbitMQ 0.1.1__tar.gz → 0.1.1.3__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.
- defstream_rabbitmq-0.1.1.3/.github/workflows/ci.yml +403 -0
- defstream_rabbitmq-0.1.1.3/.gitignore +58 -0
- defstream_rabbitmq-0.1.1.3/DefStream_RabbitMQ.egg-info/PKG-INFO +239 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/DefStream_RabbitMQ.egg-info/SOURCES.txt +6 -2
- defstream_rabbitmq-0.1.1.3/PKG-INFO +239 -0
- defstream_rabbitmq-0.1.1.3/README.en.md +210 -0
- defstream_rabbitmq-0.1.1.3/README.md +208 -0
- defstream_rabbitmq-0.1.1.3/sample/Sample.RPC.py +168 -0
- defstream_rabbitmq-0.1.1.3/sample/Sample.Stream.py +84 -0
- defstream_rabbitmq-0.1.1.3/sample/Sample.py +127 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/setup.py +7 -1
- defstream_rabbitmq-0.1.1/DefStream_RabbitMQ.egg-info/PKG-INFO +0 -143
- defstream_rabbitmq-0.1.1/MANIFEST.in +0 -1
- defstream_rabbitmq-0.1.1/PKG-INFO +0 -143
- defstream_rabbitmq-0.1.1/README.md +0 -113
- defstream_rabbitmq-0.1.1/VERSION +0 -1
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/DefStream_RabbitMQ.egg-info/dependency_links.txt +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/DefStream_RabbitMQ.egg-info/requires.txt +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/DefStream_RabbitMQ.egg-info/top_level.txt +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/LICENSE +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/setup.cfg +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Configuration.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Connection.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Consumer.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Event.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Exchange.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Policies.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Queue.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Serializer.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Stream.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/Subscription.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/src/__init__.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/test/test_broker_methods.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/test/test_rabbitmq_integration.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/test/test_rabbitmq_worker.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/test/test_rpc.py +0 -0
- {defstream_rabbitmq-0.1.1 → defstream_rabbitmq-0.1.1.3}/test/test_rpc_integration.py +0 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [master]
|
|
6
|
+
push:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
RELEASE_SERIES: '0.1'
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
17
|
+
cancel-in-progress: false
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
changes:
|
|
21
|
+
name: Plan release version
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
timeout-minutes: 5
|
|
24
|
+
outputs:
|
|
25
|
+
release: ${{ steps.changes.outputs.release }}
|
|
26
|
+
version: ${{ steps.changes.outputs.version }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
persist-credentials: false
|
|
32
|
+
- name: Set up release planner Python
|
|
33
|
+
uses: actions/setup-python@v7
|
|
34
|
+
with:
|
|
35
|
+
python-version: '3.13'
|
|
36
|
+
- name: Check complete push range and plan version
|
|
37
|
+
id: changes
|
|
38
|
+
env:
|
|
39
|
+
BEFORE: ${{ github.event.before }}
|
|
40
|
+
RELEASE_PUSH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
|
41
|
+
run: |
|
|
42
|
+
python - <<'PY'
|
|
43
|
+
from os import environ
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from re import fullmatch
|
|
46
|
+
from subprocess import check_output
|
|
47
|
+
|
|
48
|
+
if environ['RELEASE_PUSH'] != 'true':
|
|
49
|
+
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
50
|
+
output.write('release=false\n')
|
|
51
|
+
raise SystemExit(0)
|
|
52
|
+
series = environ['RELEASE_SERIES']
|
|
53
|
+
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)', series) is None:
|
|
54
|
+
raise ValueError(f'Invalid RELEASE_SERIES: {series!r}')
|
|
55
|
+
before = environ['BEFORE']
|
|
56
|
+
if not before:
|
|
57
|
+
raise ValueError('Missing push base commit')
|
|
58
|
+
first_push = set(before) == {'0'}
|
|
59
|
+
base = check_output(['git', 'hash-object', '-t', 'tree', '--stdin'], input='', text=True).strip() if first_push else before
|
|
60
|
+
changed = check_output(
|
|
61
|
+
['git', 'diff', '--name-only', '--no-renames', base, environ['GITHUB_SHA'], '--', 'src/'],
|
|
62
|
+
text=True,
|
|
63
|
+
).strip()
|
|
64
|
+
# Anchor to the pre-push history so a rerun ignores its own release tag.
|
|
65
|
+
tags = [] if first_push else check_output(['git', 'tag', '--merged', before], text=True).splitlines()
|
|
66
|
+
releases = []
|
|
67
|
+
for tag in tags:
|
|
68
|
+
match = fullmatch(r'v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?:\.post\d+)?', tag)
|
|
69
|
+
if match and f'{int(match[1])}.{int(match[2])}' == series:
|
|
70
|
+
releases.append(tuple(map(int, match.groups()[:3])) + (int(match[4] or 0),))
|
|
71
|
+
latest = max(releases) if releases else None
|
|
72
|
+
number = int(environ['GITHUB_RUN_NUMBER'])
|
|
73
|
+
if number < 1:
|
|
74
|
+
raise ValueError('Build number must be positive')
|
|
75
|
+
if latest is not None and number <= latest[3]:
|
|
76
|
+
raise ValueError('Build number must exceed the previous build; do not reset the workflow counter')
|
|
77
|
+
if changed:
|
|
78
|
+
patch = latest[2] + 1 if latest is not None else 0
|
|
79
|
+
print(f'Source changes:\n{changed}')
|
|
80
|
+
else:
|
|
81
|
+
if latest is None:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
'No release tag in pre-push history for this series. '
|
|
84
|
+
'Tag the exact source commit of the existing PyPI release before retrying.'
|
|
85
|
+
)
|
|
86
|
+
patch = latest[2]
|
|
87
|
+
print('No src changes: keeping MAJOR.MINOR.PATCH and increasing BUILD')
|
|
88
|
+
version = f'{series}.{patch}.{number}'
|
|
89
|
+
print(f'Planned version: {version}')
|
|
90
|
+
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
91
|
+
output.write(f'release=true\nversion={version}\n')
|
|
92
|
+
PY
|
|
93
|
+
|
|
94
|
+
test:
|
|
95
|
+
name: Test Python ${{ matrix.python }}
|
|
96
|
+
needs: changes
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
timeout-minutes: 25
|
|
99
|
+
strategy:
|
|
100
|
+
fail-fast: false
|
|
101
|
+
matrix:
|
|
102
|
+
python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
|
|
103
|
+
env:
|
|
104
|
+
PROJECT: ${{ github.event.repository.name }}
|
|
105
|
+
DEFSTREAM_RABBITMQ_TESTS: '1'
|
|
106
|
+
DEFSTREAM_REDIS_TESTS: '1'
|
|
107
|
+
services:
|
|
108
|
+
rabbitmq:
|
|
109
|
+
image: rabbitmq:4.1
|
|
110
|
+
ports:
|
|
111
|
+
- 5672:5672
|
|
112
|
+
env:
|
|
113
|
+
RABBITMQ_DEFAULT_USER: guest
|
|
114
|
+
RABBITMQ_DEFAULT_PASS: guest
|
|
115
|
+
RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: '-rabbit loopback_users []'
|
|
116
|
+
options: >-
|
|
117
|
+
--health-cmd "rabbitmq-diagnostics -q ping"
|
|
118
|
+
--health-interval 10s
|
|
119
|
+
--health-timeout 10s
|
|
120
|
+
--health-retries 12
|
|
121
|
+
redis:
|
|
122
|
+
image: redis:7.4
|
|
123
|
+
ports:
|
|
124
|
+
- 6379:6379
|
|
125
|
+
options: >-
|
|
126
|
+
--health-cmd "redis-cli ping"
|
|
127
|
+
--health-interval 5s
|
|
128
|
+
--health-timeout 5s
|
|
129
|
+
--health-retries 12
|
|
130
|
+
steps:
|
|
131
|
+
- name: Check out core
|
|
132
|
+
uses: actions/checkout@v7
|
|
133
|
+
with:
|
|
134
|
+
repository: yong5eon/DefStream
|
|
135
|
+
ref: ${{ github.event.repository.name == 'DefStream' && github.sha || 'master' }}
|
|
136
|
+
path: packages/DefStream
|
|
137
|
+
fetch-depth: 0
|
|
138
|
+
persist-credentials: false
|
|
139
|
+
|
|
140
|
+
- name: Check out RabbitMQ adapter
|
|
141
|
+
uses: actions/checkout@v7
|
|
142
|
+
with:
|
|
143
|
+
repository: yong5eon/DefStream-RabbitMQ
|
|
144
|
+
ref: ${{ github.event.repository.name == 'DefStream-RabbitMQ' && github.sha || 'master' }}
|
|
145
|
+
path: packages/DefStream-RabbitMQ
|
|
146
|
+
fetch-depth: 0
|
|
147
|
+
persist-credentials: false
|
|
148
|
+
|
|
149
|
+
- name: Check out Redis adapter
|
|
150
|
+
uses: actions/checkout@v7
|
|
151
|
+
with:
|
|
152
|
+
repository: yong5eon/DefStream-Redis
|
|
153
|
+
ref: ${{ github.event.repository.name == 'DefStream-Redis' && github.sha || 'master' }}
|
|
154
|
+
path: packages/DefStream-Redis
|
|
155
|
+
fetch-depth: 0
|
|
156
|
+
persist-credentials: false
|
|
157
|
+
|
|
158
|
+
- name: Set up Python
|
|
159
|
+
uses: actions/setup-python@v7
|
|
160
|
+
with:
|
|
161
|
+
python-version: ${{ matrix.python }}
|
|
162
|
+
|
|
163
|
+
- name: Record source commits and tag candidate locally
|
|
164
|
+
env:
|
|
165
|
+
RELEASE_BUILD: ${{ needs.changes.outputs.release }}
|
|
166
|
+
RELEASE_VERSION: ${{ needs.changes.outputs.version }}
|
|
167
|
+
run: |
|
|
168
|
+
for package in packages/*; do
|
|
169
|
+
echo "$package: $(git -C "$package" rev-parse HEAD)"
|
|
170
|
+
done
|
|
171
|
+
echo "SOURCE_DATE_EPOCH=$(git -C "packages/$PROJECT" show -s --format=%ct HEAD)" >> "$GITHUB_ENV"
|
|
172
|
+
python - <<'PY'
|
|
173
|
+
from os import environ
|
|
174
|
+
from pathlib import Path
|
|
175
|
+
from re import fullmatch
|
|
176
|
+
from subprocess import check_output, run
|
|
177
|
+
|
|
178
|
+
project = environ['PROJECT']
|
|
179
|
+
if project not in ('DefStream', 'DefStream-RabbitMQ', 'DefStream-Redis'):
|
|
180
|
+
raise ValueError(f'Unexpected project: {project}')
|
|
181
|
+
if environ['RELEASE_BUILD'] != 'true':
|
|
182
|
+
print('Test-only build: using the existing Git-derived development version.')
|
|
183
|
+
raise SystemExit(0)
|
|
184
|
+
version = environ['RELEASE_VERSION']
|
|
185
|
+
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\.[1-9]\d*', version) is None:
|
|
186
|
+
raise ValueError(f'Invalid planned version: {version!r}')
|
|
187
|
+
path = Path('packages') / project
|
|
188
|
+
tag = version
|
|
189
|
+
tags = check_output(['git', '-C', str(path), 'tag', '--list', tag], text=True).strip()
|
|
190
|
+
if tags:
|
|
191
|
+
tagged = check_output(['git', '-C', str(path), 'rev-parse', f'{tag}^{{commit}}'], text=True).strip()
|
|
192
|
+
if tagged != environ['GITHUB_SHA']:
|
|
193
|
+
raise RuntimeError(f'{tag} already points at another commit')
|
|
194
|
+
else:
|
|
195
|
+
run(['git', '-C', str(path), 'tag', tag, environ['GITHUB_SHA']], check=True)
|
|
196
|
+
print(f'{project}: local candidate {tag}')
|
|
197
|
+
with Path(environ['GITHUB_STEP_SUMMARY']).open('a', encoding='utf-8') as summary:
|
|
198
|
+
summary.write(f'## {project} {version}\n\nSource: `{environ["GITHUB_SHA"]}`\n')
|
|
199
|
+
PY
|
|
200
|
+
|
|
201
|
+
- name: Install build tools
|
|
202
|
+
run: python -m pip install --upgrade build twine
|
|
203
|
+
|
|
204
|
+
- name: Build source distributions and wheels
|
|
205
|
+
run: |
|
|
206
|
+
mkdir -p dist dependency-wheels
|
|
207
|
+
for name in DefStream DefStream-RabbitMQ DefStream-Redis; do
|
|
208
|
+
if [ "$name" = "$PROJECT" ]; then
|
|
209
|
+
python -m build --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
210
|
+
else
|
|
211
|
+
python -m build --outdir "$GITHUB_WORKSPACE/dependency-wheels" "packages/$name"
|
|
212
|
+
fi
|
|
213
|
+
done
|
|
214
|
+
python -m twine check --strict dist/*
|
|
215
|
+
|
|
216
|
+
- name: Install built wheels into a clean environment
|
|
217
|
+
run: |
|
|
218
|
+
python -m venv "$RUNNER_TEMP/installed"
|
|
219
|
+
"$RUNNER_TEMP/installed/bin/python" -m pip install dist/*.whl dependency-wheels/*.whl
|
|
220
|
+
"$RUNNER_TEMP/installed/bin/python" -m pip check
|
|
221
|
+
|
|
222
|
+
- name: Test installed package
|
|
223
|
+
run: |
|
|
224
|
+
cd "$RUNNER_TEMP"
|
|
225
|
+
"$RUNNER_TEMP/installed/bin/python" -m unittest discover \
|
|
226
|
+
-s "$GITHUB_WORKSPACE/packages/$PROJECT/test" -p 'test_*.py' -v
|
|
227
|
+
|
|
228
|
+
- name: Set up Node for core UI tests
|
|
229
|
+
if: env.PROJECT == 'DefStream'
|
|
230
|
+
uses: actions/setup-node@v7
|
|
231
|
+
with:
|
|
232
|
+
node-version: '22'
|
|
233
|
+
|
|
234
|
+
- name: Test core UI
|
|
235
|
+
if: env.PROJECT == 'DefStream'
|
|
236
|
+
run: node --test packages/DefStream/test/test_dashboard_ui.cjs
|
|
237
|
+
|
|
238
|
+
- name: Preserve verified release distributions
|
|
239
|
+
if: needs.changes.outputs.release == 'true' && matrix.python == '3.13'
|
|
240
|
+
uses: actions/upload-artifact@v7
|
|
241
|
+
with:
|
|
242
|
+
name: distributions
|
|
243
|
+
path: dist/*
|
|
244
|
+
if-no-files-found: error
|
|
245
|
+
retention-days: 14
|
|
246
|
+
|
|
247
|
+
publish:
|
|
248
|
+
name: Publish PyPI
|
|
249
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.changes.outputs.release == 'true'
|
|
250
|
+
needs: [changes, test]
|
|
251
|
+
runs-on: ubuntu-latest
|
|
252
|
+
timeout-minutes: 10
|
|
253
|
+
environment: pypi
|
|
254
|
+
permissions:
|
|
255
|
+
contents: write
|
|
256
|
+
steps:
|
|
257
|
+
- name: Check out release source for tagging
|
|
258
|
+
uses: actions/checkout@v7
|
|
259
|
+
with:
|
|
260
|
+
ref: ${{ github.sha }}
|
|
261
|
+
path: release-source
|
|
262
|
+
fetch-depth: 0
|
|
263
|
+
|
|
264
|
+
- name: Download verified distributions
|
|
265
|
+
uses: actions/download-artifact@v8
|
|
266
|
+
with:
|
|
267
|
+
name: distributions
|
|
268
|
+
path: dist
|
|
269
|
+
|
|
270
|
+
- name: Set up Python
|
|
271
|
+
uses: actions/setup-python@v7
|
|
272
|
+
with:
|
|
273
|
+
python-version: '3.13'
|
|
274
|
+
|
|
275
|
+
- name: Check release status
|
|
276
|
+
id: release
|
|
277
|
+
env:
|
|
278
|
+
PROJECT: ${{ github.event.repository.name }}
|
|
279
|
+
RELEASE_VERSION: ${{ needs.changes.outputs.version }}
|
|
280
|
+
run: |
|
|
281
|
+
python - <<'PY'
|
|
282
|
+
from email.parser import BytesParser
|
|
283
|
+
from json import load
|
|
284
|
+
from os import environ
|
|
285
|
+
from pathlib import Path
|
|
286
|
+
from urllib.error import HTTPError
|
|
287
|
+
from urllib.request import urlopen
|
|
288
|
+
from zipfile import ZipFile
|
|
289
|
+
|
|
290
|
+
wheels = list(Path('dist').glob('*.whl'))
|
|
291
|
+
sources = list(Path('dist').glob('*.tar.gz'))
|
|
292
|
+
if len(wheels) != 1 or len(sources) != 1:
|
|
293
|
+
raise ValueError('Expected exactly one wheel and one source distribution')
|
|
294
|
+
with ZipFile(wheels[0]) as archive:
|
|
295
|
+
metadata_path, = (name for name in archive.namelist() if name.endswith('.dist-info/METADATA'))
|
|
296
|
+
metadata = BytesParser().parsebytes(archive.read(metadata_path))
|
|
297
|
+
project = environ['PROJECT']
|
|
298
|
+
if metadata['Name'] != project:
|
|
299
|
+
raise ValueError(f'Unexpected distribution: {metadata["Name"]}')
|
|
300
|
+
version = metadata['Version']
|
|
301
|
+
expected_version = environ['RELEASE_VERSION']
|
|
302
|
+
if version != expected_version:
|
|
303
|
+
raise ValueError(f'Expected {expected_version}, got {version}')
|
|
304
|
+
try:
|
|
305
|
+
with urlopen(f'https://pypi.org/pypi/{project}/{version}/json', timeout=30) as response:
|
|
306
|
+
release = load(response)
|
|
307
|
+
except HTTPError as error:
|
|
308
|
+
if error.code != 404:
|
|
309
|
+
raise
|
|
310
|
+
exists = False
|
|
311
|
+
else:
|
|
312
|
+
expected = {wheels[0].name, sources[0].name}
|
|
313
|
+
published = {file['filename'] for file in release['urls']}
|
|
314
|
+
if int(environ['GITHUB_RUN_ATTEMPT']) == 1 or not expected.issubset(published):
|
|
315
|
+
raise RuntimeError(
|
|
316
|
+
f'{project} {version} already exists or is partially published. '
|
|
317
|
+
'Do not overwrite it; use a new run/version or repair the release manually.'
|
|
318
|
+
)
|
|
319
|
+
print(f'{project} {version}: already published; skipping this rerun')
|
|
320
|
+
exists = True
|
|
321
|
+
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
322
|
+
output.write(f'publish={str(not exists).lower()}\n')
|
|
323
|
+
output.write(f'version={version}\n')
|
|
324
|
+
PY
|
|
325
|
+
|
|
326
|
+
- name: Record verified release tag
|
|
327
|
+
env:
|
|
328
|
+
VERSION: ${{ steps.release.outputs.version }}
|
|
329
|
+
run: |
|
|
330
|
+
tag="$VERSION"
|
|
331
|
+
cd release-source
|
|
332
|
+
if git show-ref --verify --quiet "refs/tags/$tag"; then
|
|
333
|
+
if [ "$(git rev-parse "$tag^{commit}")" != "$GITHUB_SHA" ]; then
|
|
334
|
+
echo "::error::$tag already points at another commit."
|
|
335
|
+
exit 1
|
|
336
|
+
fi
|
|
337
|
+
else
|
|
338
|
+
git tag "$tag" "$GITHUB_SHA"
|
|
339
|
+
fi
|
|
340
|
+
git push origin "refs/tags/$tag"
|
|
341
|
+
|
|
342
|
+
- name: Install upload tool
|
|
343
|
+
if: steps.release.outputs.publish == 'true'
|
|
344
|
+
run: python -m pip install --upgrade twine
|
|
345
|
+
|
|
346
|
+
- name: Upload to PyPI
|
|
347
|
+
if: steps.release.outputs.publish == 'true'
|
|
348
|
+
env:
|
|
349
|
+
TWINE_USERNAME: __token__
|
|
350
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
351
|
+
run: |
|
|
352
|
+
if [ -z "$TWINE_PASSWORD" ]; then
|
|
353
|
+
echo "::error::Configure PYPI_API_TOKEN in the pypi environment or repository Actions secrets."
|
|
354
|
+
exit 1
|
|
355
|
+
fi
|
|
356
|
+
python -m twine upload --non-interactive dist/*
|
|
357
|
+
|
|
358
|
+
- name: Publish GitHub Release with verified distributions
|
|
359
|
+
env:
|
|
360
|
+
GH_TOKEN: ${{ github.token }}
|
|
361
|
+
VERSION: ${{ steps.release.outputs.version }}
|
|
362
|
+
run: |
|
|
363
|
+
python - <<'PY'
|
|
364
|
+
from json import load
|
|
365
|
+
from os import environ
|
|
366
|
+
from pathlib import Path
|
|
367
|
+
from subprocess import run
|
|
368
|
+
from urllib.error import HTTPError
|
|
369
|
+
from urllib.parse import quote
|
|
370
|
+
from urllib.request import Request, urlopen
|
|
371
|
+
|
|
372
|
+
repository = environ['GITHUB_REPOSITORY']
|
|
373
|
+
version = environ['VERSION']
|
|
374
|
+
request = Request(
|
|
375
|
+
f'{environ["GITHUB_API_URL"]}/repos/{repository}/releases/tags/{quote(version, safe="")}',
|
|
376
|
+
headers={
|
|
377
|
+
'Authorization': f'Bearer {environ["GH_TOKEN"]}',
|
|
378
|
+
'Accept': 'application/vnd.github+json',
|
|
379
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
try:
|
|
383
|
+
with urlopen(request, timeout=30) as response:
|
|
384
|
+
release = load(response)
|
|
385
|
+
except HTTPError as error:
|
|
386
|
+
if error.code != 404:
|
|
387
|
+
raise
|
|
388
|
+
run([
|
|
389
|
+
'gh', 'release', 'create', version, '--repo', repository,
|
|
390
|
+
'--verify-tag', '--title', version, '--generate-notes', '--draft',
|
|
391
|
+
], check=True)
|
|
392
|
+
else:
|
|
393
|
+
if release['tag_name'] != version:
|
|
394
|
+
raise ValueError('GitHub Release does not match the verified version')
|
|
395
|
+
artifacts = sorted(str(path) for path in Path('dist').iterdir() if path.is_file())
|
|
396
|
+
run([
|
|
397
|
+
'gh', 'release', 'upload', version, '--repo', repository,
|
|
398
|
+
'--clobber', *artifacts,
|
|
399
|
+
], check=True)
|
|
400
|
+
run([
|
|
401
|
+
'gh', 'release', 'edit', version, '--repo', repository, '--draft=false',
|
|
402
|
+
], check=True)
|
|
403
|
+
PY
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Python bytecode and caches
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
.tox/
|
|
9
|
+
.nox/
|
|
10
|
+
.coverage
|
|
11
|
+
.coverage.*
|
|
12
|
+
|
|
13
|
+
# Virtual environments and local environment files
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
.env
|
|
18
|
+
.env.*
|
|
19
|
+
|
|
20
|
+
# Python packaging and build artifacts
|
|
21
|
+
build/
|
|
22
|
+
dist/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
*.egg
|
|
25
|
+
.Python
|
|
26
|
+
|
|
27
|
+
# Operating system files
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
Desktop.ini
|
|
31
|
+
|
|
32
|
+
# Miscellaneous files
|
|
33
|
+
*.log
|
|
34
|
+
*.tmp
|
|
35
|
+
*.bak
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
|
|
39
|
+
# Temporary files
|
|
40
|
+
tmp/
|
|
41
|
+
*.tmp
|
|
42
|
+
*.temp
|
|
43
|
+
|
|
44
|
+
# IDE and editor specific files
|
|
45
|
+
.vscode/
|
|
46
|
+
.idea/
|
|
47
|
+
|
|
48
|
+
# Git
|
|
49
|
+
.git/
|
|
50
|
+
|
|
51
|
+
# GitHub specific files
|
|
52
|
+
.github/
|
|
53
|
+
!/.github/
|
|
54
|
+
/.github/*
|
|
55
|
+
!/.github/workflows/
|
|
56
|
+
|
|
57
|
+
# GitLab specific files
|
|
58
|
+
.gitlab/
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: DefStream-RabbitMQ
|
|
3
|
+
Version: 0.1.1.3
|
|
4
|
+
Summary: RabbitMQ broker adapter for DefStream
|
|
5
|
+
Home-page: https://github.com/yong5eon/DefStream-RabbitMQ
|
|
6
|
+
Author: Heo Yongseon
|
|
7
|
+
Project-URL: Source, https://github.com/yong5eon/DefStream-RabbitMQ
|
|
8
|
+
Project-URL: Issues, https://github.com/yong5eon/DefStream-RabbitMQ/issues
|
|
9
|
+
Project-URL: Releases, https://github.com/yong5eon/DefStream-RabbitMQ/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
|
+
Requires-Dist: DefStream>=0.1.0
|
|
20
|
+
Requires-Dist: pika>=1.3.2
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: project-url
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# DefStream-RabbitMQ
|
|
33
|
+
|
|
34
|
+
**한국어** | [English](./README.en.md)
|
|
35
|
+
|
|
36
|
+
DefStream에서 RabbitMQ를 사용할 수 있게 해주는 브로커 어댑터입니다.
|
|
37
|
+
일반 큐, Exchange 라우팅, 스트림 이력 읽기, 동기 RPC를 제공하며,
|
|
38
|
+
DefStream Worker에 연결해 작업을 스레드·프로세스 풀에서 실행할 수 있습니다.
|
|
39
|
+
내부 클라이언트로 Pika를 사용합니다.
|
|
40
|
+
|
|
41
|
+
## 설치
|
|
42
|
+
|
|
43
|
+
Python **3.9 이상**과 접속 가능한 RabbitMQ 서버가 필요합니다.
|
|
44
|
+
|
|
45
|
+
```shell
|
|
46
|
+
python -m pip install DefStream-RabbitMQ
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`DefStream>=0.1.0`과 `pika>=1.3.2`도 함께 설치됩니다.
|
|
50
|
+
배포 패키지 이름은 `DefStream-RabbitMQ`이고 import 경로는
|
|
51
|
+
`DefStream.Broker.Implements.RabbitMQ`입니다.
|
|
52
|
+
|
|
53
|
+
## 큐에 보내고 받기
|
|
54
|
+
|
|
55
|
+
로컬 RabbitMQ의 `guest` 계정을 사용하는 예제입니다.
|
|
56
|
+
운영에서는 별도 계정과 환경변수·비밀 저장소를 사용하세요.
|
|
57
|
+
예제는 `defstream.example.rabbitmq` 큐를 만들며 실행 후에도 큐를 유지합니다.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
connection = Connection(Configuration(
|
|
64
|
+
host='127.0.0.1',
|
|
65
|
+
port=5672,
|
|
66
|
+
username='guest',
|
|
67
|
+
password='guest',
|
|
68
|
+
))
|
|
69
|
+
try:
|
|
70
|
+
connection.connect()
|
|
71
|
+
connection.create_queue('defstream.example.rabbitmq')
|
|
72
|
+
queue = connection.queue('defstream.example.rabbitmq')
|
|
73
|
+
queue.send({'message': 'Hello, DefStream!'})
|
|
74
|
+
event = queue.pop(timeout=1000)
|
|
75
|
+
if event is not None:
|
|
76
|
+
print(event.body)
|
|
77
|
+
event.ack()
|
|
78
|
+
finally:
|
|
79
|
+
connection.close()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
메시지는 기본 JSON 코덱으로 직렬화됩니다.
|
|
83
|
+
`pop()` 제한 시간은 **밀리초**이며, 양수 제한 시간에 메시지가 없으면 `None`을 반환합니다.
|
|
84
|
+
`None` 또는 `0`은 무기한 대기입니다.
|
|
85
|
+
처리가 성공하면 `ack()`, 재전달하려면 `nack()`, 거부하려면 `reject()`를 호출하세요.
|
|
86
|
+
거부한 메시지는 브로커의 dead-letter 설정이 있으면 해당 목적지로, 없으면 폐기됩니다.
|
|
87
|
+
|
|
88
|
+
## 연결 설정
|
|
89
|
+
|
|
90
|
+
| 옵션 | 의미 |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `host`, `port` | RabbitMQ 주소와 AMQP 포트 |
|
|
93
|
+
| `username`, `password` | 인증 정보 |
|
|
94
|
+
| `vhost` | 가상 호스트, 기본값 `/` |
|
|
95
|
+
| `ssl` | TLS 사용 여부, 기본값 `False` |
|
|
96
|
+
| `timeout` | blocked-connection 제한 시간, 밀리초 |
|
|
97
|
+
| `heartbeat` | 현재 구현은 `int(heartbeat * 2 / 1000)`초로 변환하여 Pika에 전달 |
|
|
98
|
+
| `encode`, `decode` | 메시지 코덱, 기본값 JSON |
|
|
99
|
+
|
|
100
|
+
`timeout`은 모든 작업에 대한 전체 실행 제한 시간이 아닙니다.
|
|
101
|
+
일반 연결의 heartbeat는 애플리케이션 작업·유휴 시간에 맞게 설정하고 처리되도록 관리하세요.
|
|
102
|
+
연결과 그 리소스는 **소유 스레드에서만 사용**해야 하며, 스레드·프로세스 사이에 공유하지 마세요.
|
|
103
|
+
|
|
104
|
+
## Worker로 작업 처리하기
|
|
105
|
+
|
|
106
|
+
다음은 큐와 오류 큐를 준비한 후 작업을 소비하는 실행 파일입니다.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from signal import SIGINT, signal
|
|
110
|
+
|
|
111
|
+
from DefStream.Broker.Implements.RabbitMQ import (
|
|
112
|
+
Configuration,
|
|
113
|
+
Connection,
|
|
114
|
+
Reject,
|
|
115
|
+
Subscription,
|
|
116
|
+
)
|
|
117
|
+
from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Add(
|
|
121
|
+
Run,
|
|
122
|
+
parameters=Subscription(
|
|
123
|
+
queue='defstream.example.rabbitmq.add',
|
|
124
|
+
qos=4,
|
|
125
|
+
error='defstream.example.rabbitmq.add.error',
|
|
126
|
+
),
|
|
127
|
+
on_error=Reject(),
|
|
128
|
+
):
|
|
129
|
+
def run(
|
|
130
|
+
self,
|
|
131
|
+
a: int,
|
|
132
|
+
b: int,
|
|
133
|
+
):
|
|
134
|
+
return a + b
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Prepare(WorkerSetup):
|
|
138
|
+
def __call__(
|
|
139
|
+
self,
|
|
140
|
+
worker: Worker,
|
|
141
|
+
) -> None:
|
|
142
|
+
for task in worker.tasks:
|
|
143
|
+
declare(
|
|
144
|
+
task=task,
|
|
145
|
+
broker=worker.broker,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == '__main__':
|
|
150
|
+
worker = Worker(
|
|
151
|
+
Connection(Configuration(
|
|
152
|
+
host='127.0.0.1',
|
|
153
|
+
port=5672,
|
|
154
|
+
username='guest',
|
|
155
|
+
password='guest',
|
|
156
|
+
)),
|
|
157
|
+
ThreadPool(4),
|
|
158
|
+
setups=Prepare(),
|
|
159
|
+
)
|
|
160
|
+
worker.add(Add)
|
|
161
|
+
signal(
|
|
162
|
+
SIGINT,
|
|
163
|
+
lambda *_: worker.stop(),
|
|
164
|
+
)
|
|
165
|
+
worker.run()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
다른 프로세스에서 같은 큐에 `{'a': 10, 'b': 20}`을 보내면 처리됩니다.
|
|
169
|
+
Worker는 실행과 완료 훅이 끝난 뒤 ACK하고, 실패하면 지정한 정책을 적용합니다.
|
|
170
|
+
`on_error`를 생략하면 기본값은 `Requeue()`입니다.
|
|
171
|
+
|
|
172
|
+
`Subscription` 옵션:
|
|
173
|
+
|
|
174
|
+
- `queue`: 작업 큐 이름.
|
|
175
|
+
- `qos`: 이 구독의 미확인 메시지 수 제한. 기본 소비자 값은 1이며, 0은 무제한입니다.
|
|
176
|
+
Worker의 공유 풀 크기와 별개입니다.
|
|
177
|
+
- `error`: `declare()`가 생성하고 연결할 오류 큐 이름. 거부된 메시지의 DLQ입니다.
|
|
178
|
+
- `offset`: RabbitMQ 스트림 구독에 사용할 정수 오프셋.
|
|
179
|
+
|
|
180
|
+
`Subscription` 객체 생성이나 `consumer.subs()`는 큐를 선언하지 않습니다.
|
|
181
|
+
작업용 `declare()`는 일반 durable 큐와 선택적 DLQ를 준비하며,
|
|
182
|
+
스트림 큐나 Exchange 바인딩을 자동 생성하지 않습니다.
|
|
183
|
+
기존 큐의 선언 옵션이 다르면 실패하며 자동 삭제·재생성하지 않습니다.
|
|
184
|
+
|
|
185
|
+
## 동기 RPC
|
|
186
|
+
|
|
187
|
+
실행 중인 Worker가 있는 큐에 `call()`을 사용하면 `Run.run()`의 반환값을 받습니다.
|
|
188
|
+
아래 코드는 앞의 직접 연결 예제에서 `connect()`와 `close()` 사이에 사용하세요.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
result = connection.queue('defstream.example.rabbitmq.add').call(
|
|
192
|
+
{'a': 10, 'b': 20},
|
|
193
|
+
timeout=5,
|
|
194
|
+
)
|
|
195
|
+
print(result)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- `timeout`은 **초** 단위이며 응답이 없으면 `TimeoutError`가 발생합니다.
|
|
199
|
+
이미 전달된 서버 작업을 취소하거나 전체 연결·정리 시간을 엄격히 제한하는 값은 아닙니다.
|
|
200
|
+
- 각 호출은 전용 연결과 임시 응답 큐를 사용하고 종료 시 정리합니다.
|
|
201
|
+
- 서버가 요청을 거부하면 `DefStream.Broker.RemoteError`가 발생합니다.
|
|
202
|
+
재큐잉은 최종 오류 응답 없이 재시도할 수 있습니다.
|
|
203
|
+
- 응답 발행과 요청 ACK는 원자적이지 않습니다. RPC에서도 중복 실행에 대비하세요.
|
|
204
|
+
|
|
205
|
+
공유 `Run` 클래스를 사용하는 애플리케이션은 코어의
|
|
206
|
+
`request(body, task=Add, broker=connection, timeout=5)`도 사용할 수 있습니다.
|
|
207
|
+
|
|
208
|
+
## Exchange와 스트림
|
|
209
|
+
|
|
210
|
+
- `connection.exchange(name)`은 `send()`와 `call()`을 제공합니다.
|
|
211
|
+
`event=`가 라우팅 키입니다. Exchange와 큐·바인딩을 먼저 선언하세요.
|
|
212
|
+
- Exchange RPC가 여러 큐로 라우팅되면 여러 곳에서 실행될 수 있으며,
|
|
213
|
+
클라이언트는 첫 일치 응답만 받습니다. 일반 RPC에는 단일 목적지를 사용하세요.
|
|
214
|
+
- `create_exchange()`, `bind_exchange_to_queue()`, `bind_exchange_to_exchange()`로 라우팅을 구성합니다.
|
|
215
|
+
각각에 대응하는 `delete_...` 또는 `unbind_...` 메서드도 있습니다.
|
|
216
|
+
- `create_stream()`으로 스트림 큐를 만든 뒤 `connection.stream(name)`의
|
|
217
|
+
`send()`와 `read(offset=0, timeout=1000)`을 사용합니다. 읽기 제한 시간은 밀리초입니다.
|
|
218
|
+
읽은 이벤트는 이터레이터가 다음 단계로 진행될 때 자동 ACK합니다.
|
|
219
|
+
- `delete_queue()`, `delete_stream()`, `delete_exchange()`는 리소스를 삭제하므로
|
|
220
|
+
애플리케이션 소유 리소스에만 사용하세요.
|
|
221
|
+
|
|
222
|
+
## 운영 시 주의사항
|
|
223
|
+
|
|
224
|
+
- 최소 한 번(at-least-once) 전달을 전제로 멱등 작업을 작성하세요.
|
|
225
|
+
- 무제한 재큐잉은 영구 오류 메시지의 반복 실행을 만들 수 있습니다. DLQ와 재시도 정책을 설계하세요.
|
|
226
|
+
- 소비자 연결에서 긴 작업을 직접 실행하지 말고 Worker의 풀을 사용하세요.
|
|
227
|
+
- 작업 훅에서 발행하려면 훅이 실행되는 스레드·프로세스 안에 별도 생산자 연결을 만드세요.
|
|
228
|
+
- `worker.stop()`은 처리 중인 작업과 ACK를 기다린 뒤 연결과 풀을 닫습니다.
|
|
229
|
+
강제 종료 또는 종료 대기 시간 제한은 제공하지 않습니다.
|
|
230
|
+
|
|
231
|
+
## 더 알아보기
|
|
232
|
+
|
|
233
|
+
- [DefStream 사용 설명서](https://github.com/yong5eon/DefStream#readme): 작업, 풀, 훅, 검증, Inspector, 대시보드·콘솔
|
|
234
|
+
- [큐·Exchange 예제](./sample/Sample.py)
|
|
235
|
+
- [독립 실행 RPC 예제](./sample/Sample.RPC.py)
|
|
236
|
+
- [스트림 예제](./sample/Sample.Stream.py)
|
|
237
|
+
- [릴리즈와 변경 내역](https://github.com/yong5eon/DefStream-RabbitMQ/releases)
|
|
238
|
+
|
|
239
|
+
GNU GPL v3 라이선스를 사용합니다. 자세한 조건은 [LICENSE](./LICENSE)를 확인하세요.
|