DefStream-RabbitMQ 0.1.2.6__tar.gz → 0.1.4__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.
Potentially problematic release.
This version of DefStream-RabbitMQ might be problematic. Click here for more details.
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/.github/workflows/ci.yml +210 -107
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4/DefStream_RabbitMQ.egg-info}/PKG-INFO +8 -3
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/DefStream_RabbitMQ.egg-info/SOURCES.txt +4 -0
- defstream_rabbitmq-0.1.4/DefStream_RabbitMQ.egg-info/requires.txt +2 -0
- {defstream_rabbitmq-0.1.2.6/DefStream_RabbitMQ.egg-info → defstream_rabbitmq-0.1.4}/PKG-INFO +8 -3
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/README.en.md +6 -1
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/README.md +6 -1
- defstream_rabbitmq-0.1.4/issues/fix/3/IMPLEMENTS.md +22 -0
- defstream_rabbitmq-0.1.4/issues/fix/3/README.md +20 -0
- defstream_rabbitmq-0.1.4/issues/fix/3/REQUIREMENTS.md +8 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/setup.py +1 -1
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Connection.py +1 -5
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/test/test_priorities.py +4 -4
- defstream_rabbitmq-0.1.4/test/test_release_workflow.py +398 -0
- defstream_rabbitmq-0.1.2.6/DefStream_RabbitMQ.egg-info/requires.txt +0 -2
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/.gitignore +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/DefStream_RabbitMQ.egg-info/dependency_links.txt +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/DefStream_RabbitMQ.egg-info/top_level.txt +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/LICENSE +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/issues/feature/1/IMPLEMENTS.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/issues/feature/1/README.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/issues/feature/1/REQUIREMENTS.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/issues/feature/1/TASK.md +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/issues/feature/1/ci.refs.json +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/sample/Sample.RPC.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/sample/Sample.Stream.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/sample/Sample.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/setup.cfg +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Configuration.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Consumer.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Event.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Exchange.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Policies.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Queue.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Serializer.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Stream.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/Subscription.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/src/__init__.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/test/test_broker_methods.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/test/test_rabbitmq_integration.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/test/test_rabbitmq_worker.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/test/test_rpc.py +0 -0
- {defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/test/test_rpc_integration.py +0 -0
|
@@ -27,6 +27,9 @@ jobs:
|
|
|
27
27
|
outputs:
|
|
28
28
|
release: ${{ steps.changes.outputs.release }}
|
|
29
29
|
version: ${{ steps.changes.outputs.version }}
|
|
30
|
+
build: ${{ steps.changes.outputs.build }}
|
|
31
|
+
new_version: ${{ steps.changes.outputs.new_version }}
|
|
32
|
+
tag: ${{ steps.changes.outputs.tag }}
|
|
30
33
|
core_ref: ${{ steps.refs.outputs.core_ref }}
|
|
31
34
|
rabbitmq_ref: ${{ steps.refs.outputs.rabbitmq_ref }}
|
|
32
35
|
redis_ref: ${{ steps.refs.outputs.redis_ref }}
|
|
@@ -39,10 +42,80 @@ jobs:
|
|
|
39
42
|
uses: actions/setup-python@v7
|
|
40
43
|
with:
|
|
41
44
|
python-version: '3.13'
|
|
45
|
+
- name: Check complete push range and plan version
|
|
46
|
+
id: changes
|
|
47
|
+
env:
|
|
48
|
+
BEFORE: ${{ github.event.before }}
|
|
49
|
+
RELEASE_PUSH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
|
50
|
+
run: |
|
|
51
|
+
python - <<'PY'
|
|
52
|
+
from json import loads
|
|
53
|
+
from os import environ
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from re import fullmatch
|
|
56
|
+
from subprocess import check_output
|
|
57
|
+
|
|
58
|
+
release_push = environ['RELEASE_PUSH'] == 'true'
|
|
59
|
+
series = environ['RELEASE_SERIES']
|
|
60
|
+
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)', series) is None:
|
|
61
|
+
raise ValueError(f'Invalid RELEASE_SERIES: {series!r}')
|
|
62
|
+
before = environ['BEFORE']
|
|
63
|
+
if not release_push:
|
|
64
|
+
event = loads(Path(environ['GITHUB_EVENT_PATH']).read_text(encoding='utf-8'))
|
|
65
|
+
before = event['pull_request']['base']['sha']
|
|
66
|
+
if not before:
|
|
67
|
+
raise ValueError('Missing push base commit')
|
|
68
|
+
first_push = set(before) == {'0'}
|
|
69
|
+
base = check_output(['git', 'hash-object', '-t', 'tree', '--stdin'], input='', text=True).strip() if first_push else before
|
|
70
|
+
# 재실행 시 이번 실행에서 만든 태그는 제외한다.
|
|
71
|
+
tags = [] if first_push else check_output(['git', 'tag', '--merged', before], text=True).splitlines()
|
|
72
|
+
releases = []
|
|
73
|
+
release_tags = {}
|
|
74
|
+
for tag in tags:
|
|
75
|
+
match = fullmatch(r'v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?:\.post(\d+))?', tag)
|
|
76
|
+
if match and f'{int(match[1])}.{int(match[2])}' == series:
|
|
77
|
+
release = tuple(map(int, match.groups()[:3])) + (
|
|
78
|
+
int(match[4] or 0), int(match[5] or 0), match[4] is not None or match[5] is not None,
|
|
79
|
+
)
|
|
80
|
+
releases.append(release)
|
|
81
|
+
release_tags[release] = tag
|
|
82
|
+
latest = max(releases) if releases else None
|
|
83
|
+
if latest is not None:
|
|
84
|
+
base = release_tags[latest]
|
|
85
|
+
changed = check_output(
|
|
86
|
+
[
|
|
87
|
+
'git', 'diff', '--name-only', '--no-renames', base, environ['GITHUB_SHA'],
|
|
88
|
+
'--', ':(glob)src/**/*.py', 'setup.py', 'setup.cfg', 'pyproject.toml', 'MANIFEST.in',
|
|
89
|
+
],
|
|
90
|
+
text=True,
|
|
91
|
+
).strip()
|
|
92
|
+
number = int(environ['GITHUB_RUN_NUMBER'])
|
|
93
|
+
if number < 1:
|
|
94
|
+
raise ValueError('Build number must be positive')
|
|
95
|
+
if latest is None and not changed:
|
|
96
|
+
raise ValueError('No release tag or source/packaging changes; tag the existing release before rebuilding')
|
|
97
|
+
migrate = latest is not None and latest[5]
|
|
98
|
+
new_version = bool(changed) or latest is None or migrate
|
|
99
|
+
if new_version:
|
|
100
|
+
patch = latest[2] + 1 if latest is not None else 0
|
|
101
|
+
print(f'New public version: source/packaging changes or initial/migration release\n{changed}')
|
|
102
|
+
else:
|
|
103
|
+
patch = latest[2]
|
|
104
|
+
print('Keeping MAJOR.MINOR.PATCH; publishing a new wheel build')
|
|
105
|
+
version = f'{series}.{patch}'
|
|
106
|
+
print(f'Planned version: {version}, wheel build: {number}')
|
|
107
|
+
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
108
|
+
output.write(f'release={str(release_push).lower()}\nversion={version}\n')
|
|
109
|
+
output.write(f'build={number}\nnew_version={str(new_version).lower()}\n')
|
|
110
|
+
output.write(f'tag={version if new_version else release_tags[latest]}\n')
|
|
111
|
+
PY
|
|
42
112
|
- name: Validate PR branch changelog
|
|
43
113
|
id: changelog
|
|
44
114
|
env:
|
|
45
115
|
GH_TOKEN: ${{ github.token }}
|
|
116
|
+
BEFORE: ${{ github.event.before }}
|
|
117
|
+
NEW_VERSION: ${{ steps.changes.outputs.new_version }}
|
|
118
|
+
RELEASE_TAG: ${{ steps.changes.outputs.tag }}
|
|
46
119
|
run: |
|
|
47
120
|
python - <<'PY'
|
|
48
121
|
from json import loads
|
|
@@ -67,20 +140,38 @@ jobs:
|
|
|
67
140
|
and pull['base']['ref'] == 'master'
|
|
68
141
|
and pull['base']['repo']['full_name'] == repository
|
|
69
142
|
]
|
|
70
|
-
if len(pulls)
|
|
71
|
-
raise ValueError('
|
|
72
|
-
branch = pulls[0]['head']['ref']
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
143
|
+
if len(pulls) > 1:
|
|
144
|
+
raise ValueError('Multiple merged PRs found for the release commit')
|
|
145
|
+
branch = pulls[0]['head']['ref'] if pulls else 'master'
|
|
146
|
+
if environ['NEW_VERSION'] == 'false':
|
|
147
|
+
tag = environ['RELEASE_TAG']
|
|
148
|
+
release = loads(check_output([
|
|
149
|
+
'gh', 'api', f'repos/{environ["GITHUB_REPOSITORY"]}/releases/tags/{tag}',
|
|
150
|
+
]))
|
|
151
|
+
if release['tag_name'] != tag or not release.get('body', '').strip():
|
|
152
|
+
raise ValueError(f'Existing release notes are missing or empty: {tag}')
|
|
153
|
+
body = release['body'].encode('utf-8')
|
|
154
|
+
print(f'Reusing release notes unchanged: {tag}')
|
|
155
|
+
elif environ['GITHUB_EVENT_NAME'] == 'push' and not pulls:
|
|
156
|
+
before = environ['BEFORE']
|
|
157
|
+
if not before:
|
|
158
|
+
raise ValueError('Missing push base commit')
|
|
159
|
+
revision = head if set(before) == {'0'} else f'{before}..{head}'
|
|
160
|
+
commits = check_output(['git', 'log', '--reverse', '--format=- %s (%h)', revision])
|
|
161
|
+
body = '## master 직접 push\n\n'.encode('utf-8') + commits
|
|
162
|
+
print('Direct master push: generating release notes from commits')
|
|
163
|
+
else:
|
|
164
|
+
path = f'issues/{branch}/README.md'
|
|
165
|
+
entry = check_output(['git', 'ls-tree', head, '--', path]).decode().split()
|
|
166
|
+
if not entry or entry[0] not in ('100644', '100755'):
|
|
167
|
+
raise ValueError(f'Changelog must exist as a regular file: {path}')
|
|
168
|
+
body = check_output(['git', 'show', f'{head}:{path}'])
|
|
169
|
+
if not body.decode('utf-8').strip():
|
|
170
|
+
raise ValueError(f'Customer changelog is empty: {path}')
|
|
171
|
+
print(f'Customer changelog: {path}')
|
|
80
172
|
notes = Path(environ['RUNNER_TEMP'], 'release-notes', 'README.md')
|
|
81
173
|
notes.parent.mkdir(parents=True, exist_ok=True)
|
|
82
174
|
notes.write_bytes(body)
|
|
83
|
-
print(f'Customer changelog: {path}')
|
|
84
175
|
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
85
176
|
output.write(f'branch={branch}\n')
|
|
86
177
|
PY
|
|
@@ -129,63 +220,6 @@ jobs:
|
|
|
129
220
|
if-no-files-found: error
|
|
130
221
|
retention-days: 14
|
|
131
222
|
overwrite: true
|
|
132
|
-
- name: Check complete push range and plan version
|
|
133
|
-
id: changes
|
|
134
|
-
env:
|
|
135
|
-
BEFORE: ${{ github.event.before }}
|
|
136
|
-
RELEASE_PUSH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
|
137
|
-
run: |
|
|
138
|
-
python - <<'PY'
|
|
139
|
-
from os import environ
|
|
140
|
-
from pathlib import Path
|
|
141
|
-
from re import fullmatch
|
|
142
|
-
from subprocess import check_output
|
|
143
|
-
|
|
144
|
-
if environ['RELEASE_PUSH'] != 'true':
|
|
145
|
-
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
146
|
-
output.write('release=false\n')
|
|
147
|
-
raise SystemExit(0)
|
|
148
|
-
series = environ['RELEASE_SERIES']
|
|
149
|
-
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)', series) is None:
|
|
150
|
-
raise ValueError(f'Invalid RELEASE_SERIES: {series!r}')
|
|
151
|
-
before = environ['BEFORE']
|
|
152
|
-
if not before:
|
|
153
|
-
raise ValueError('Missing push base commit')
|
|
154
|
-
first_push = set(before) == {'0'}
|
|
155
|
-
base = check_output(['git', 'hash-object', '-t', 'tree', '--stdin'], input='', text=True).strip() if first_push else before
|
|
156
|
-
changed = check_output(
|
|
157
|
-
['git', 'diff', '--name-only', '--no-renames', base, environ['GITHUB_SHA'], '--', 'src/'],
|
|
158
|
-
text=True,
|
|
159
|
-
).strip()
|
|
160
|
-
# Anchor to the pre-push history so a rerun ignores its own release tag.
|
|
161
|
-
tags = [] if first_push else check_output(['git', 'tag', '--merged', before], text=True).splitlines()
|
|
162
|
-
releases = []
|
|
163
|
-
for tag in tags:
|
|
164
|
-
match = fullmatch(r'v?(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?:\.post\d+)?', tag)
|
|
165
|
-
if match and f'{int(match[1])}.{int(match[2])}' == series:
|
|
166
|
-
releases.append(tuple(map(int, match.groups()[:3])) + (int(match[4] or 0),))
|
|
167
|
-
latest = max(releases) if releases else None
|
|
168
|
-
number = int(environ['GITHUB_RUN_NUMBER'])
|
|
169
|
-
if number < 1:
|
|
170
|
-
raise ValueError('Build number must be positive')
|
|
171
|
-
if latest is not None and number <= latest[3]:
|
|
172
|
-
raise ValueError('Build number must exceed the previous build; do not reset the workflow counter')
|
|
173
|
-
if changed:
|
|
174
|
-
patch = latest[2] + 1 if latest is not None else 0
|
|
175
|
-
print(f'Source changes:\n{changed}')
|
|
176
|
-
else:
|
|
177
|
-
if latest is None:
|
|
178
|
-
raise ValueError(
|
|
179
|
-
'No release tag in pre-push history for this series. '
|
|
180
|
-
'Tag the exact source commit of the existing PyPI release before retrying.'
|
|
181
|
-
)
|
|
182
|
-
patch = latest[2]
|
|
183
|
-
print('No src changes: keeping MAJOR.MINOR.PATCH and increasing BUILD')
|
|
184
|
-
version = f'{series}.{patch}.{number}'
|
|
185
|
-
print(f'Planned version: {version}')
|
|
186
|
-
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
187
|
-
output.write(f'release=true\nversion={version}\n')
|
|
188
|
-
PY
|
|
189
223
|
|
|
190
224
|
test:
|
|
191
225
|
name: Test Python ${{ matrix.python }}
|
|
@@ -256,7 +290,7 @@ jobs:
|
|
|
256
290
|
with:
|
|
257
291
|
python-version: ${{ matrix.python }}
|
|
258
292
|
|
|
259
|
-
- name: Record source commits and
|
|
293
|
+
- name: Record source commits and configure candidate version
|
|
260
294
|
env:
|
|
261
295
|
RELEASE_BUILD: ${{ needs.changes.outputs.release }}
|
|
262
296
|
RELEASE_VERSION: ${{ needs.changes.outputs.version }}
|
|
@@ -269,7 +303,6 @@ jobs:
|
|
|
269
303
|
from os import environ
|
|
270
304
|
from pathlib import Path
|
|
271
305
|
from re import fullmatch
|
|
272
|
-
from subprocess import check_output, run
|
|
273
306
|
|
|
274
307
|
project = environ['PROJECT']
|
|
275
308
|
if project not in ('DefStream', 'DefStream-RabbitMQ', 'DefStream-Redis'):
|
|
@@ -278,37 +311,70 @@ jobs:
|
|
|
278
311
|
print('Test-only build: using the existing Git-derived development version.')
|
|
279
312
|
raise SystemExit(0)
|
|
280
313
|
version = environ['RELEASE_VERSION']
|
|
281
|
-
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)
|
|
314
|
+
if fullmatch(r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)', version) is None:
|
|
282
315
|
raise ValueError(f'Invalid planned version: {version!r}')
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if tags:
|
|
287
|
-
tagged = check_output(['git', '-C', str(path), 'rev-parse', f'{tag}^{{commit}}'], text=True).strip()
|
|
288
|
-
if tagged != environ['GITHUB_SHA']:
|
|
289
|
-
raise RuntimeError(f'{tag} already points at another commit')
|
|
290
|
-
else:
|
|
291
|
-
run(['git', '-C', str(path), 'tag', tag, environ['GITHUB_SHA']], check=True)
|
|
292
|
-
print(f'{project}: local candidate {tag}')
|
|
316
|
+
with Path(environ['GITHUB_ENV']).open('a', encoding='utf-8') as output:
|
|
317
|
+
output.write(f'RELEASE_VERSION={version}\n')
|
|
318
|
+
print(f'{project}: candidate {version}')
|
|
293
319
|
with Path(environ['GITHUB_STEP_SUMMARY']).open('a', encoding='utf-8') as summary:
|
|
294
320
|
summary.write(f'## {project} {version}\n\nSource: `{environ["GITHUB_SHA"]}`\n')
|
|
295
321
|
PY
|
|
296
322
|
|
|
297
323
|
- name: Install build tools
|
|
298
|
-
run: python -m pip install --upgrade build twine
|
|
324
|
+
run: python -m pip install --upgrade build twine wheel
|
|
299
325
|
|
|
300
326
|
- name: Build source distributions and wheels
|
|
327
|
+
env:
|
|
328
|
+
RELEASE_BUILD: ${{ needs.changes.outputs.release }}
|
|
329
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
330
|
+
WHEEL_BUILD: ${{ needs.changes.outputs.build }}
|
|
301
331
|
run: |
|
|
302
332
|
mkdir -p dist dependency-wheels
|
|
303
333
|
for name in DefStream DefStream-RabbitMQ DefStream-Redis; do
|
|
304
334
|
if [ "$name" = "$PROJECT" ]; then
|
|
305
|
-
|
|
335
|
+
if [ "$RELEASE_BUILD" = "true" ]; then
|
|
336
|
+
if [ "$NEW_VERSION" = "true" ]; then
|
|
337
|
+
SETUPTOOLS_SCM_PRETEND_VERSION="$RELEASE_VERSION" python -m build --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
338
|
+
else
|
|
339
|
+
SETUPTOOLS_SCM_PRETEND_VERSION="$RELEASE_VERSION" python -m build --wheel --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
340
|
+
fi
|
|
341
|
+
python -m wheel tags --build "$WHEEL_BUILD" --remove dist/*.whl
|
|
342
|
+
else
|
|
343
|
+
python -m build --outdir "$GITHUB_WORKSPACE/dist" "packages/$name"
|
|
344
|
+
fi
|
|
306
345
|
else
|
|
307
346
|
python -m build --outdir "$GITHUB_WORKSPACE/dependency-wheels" "packages/$name"
|
|
308
347
|
fi
|
|
309
348
|
done
|
|
310
349
|
python -m twine check --strict dist/*
|
|
311
350
|
|
|
351
|
+
- name: Normalize source archive timestamps
|
|
352
|
+
run: |
|
|
353
|
+
python - <<'PY'
|
|
354
|
+
from gzip import GzipFile
|
|
355
|
+
from io import BytesIO
|
|
356
|
+
from os import environ
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
from tarfile import open as open_tar
|
|
359
|
+
|
|
360
|
+
epoch = int(environ['SOURCE_DATE_EPOCH'])
|
|
361
|
+
for path in Path('dist').glob('*.tar.gz'):
|
|
362
|
+
with BytesIO() as buffer:
|
|
363
|
+
with open_tar(path, 'r:gz') as source:
|
|
364
|
+
with GzipFile(fileobj=buffer, mode='wb', filename='', mtime=epoch) as compressed:
|
|
365
|
+
with open_tar(fileobj=compressed, mode='w') as target:
|
|
366
|
+
for member in sorted(source.getmembers(), key=lambda item: item.name):
|
|
367
|
+
member.mtime = epoch
|
|
368
|
+
member.uid = member.gid = 0
|
|
369
|
+
member.uname = member.gname = ''
|
|
370
|
+
member.pax_headers = {
|
|
371
|
+
key: value for key, value in member.pax_headers.items()
|
|
372
|
+
if key not in ('mtime', 'atime', 'ctime')
|
|
373
|
+
}
|
|
374
|
+
target.addfile(member, source.extractfile(member) if member.isfile() else None)
|
|
375
|
+
path.write_bytes(buffer.getvalue())
|
|
376
|
+
PY
|
|
377
|
+
|
|
312
378
|
- name: Install built wheels into a clean environment
|
|
313
379
|
run: |
|
|
314
380
|
python -m venv "$RUNNER_TEMP/installed"
|
|
@@ -379,20 +445,27 @@ jobs:
|
|
|
379
445
|
env:
|
|
380
446
|
PROJECT: ${{ github.event.repository.name }}
|
|
381
447
|
RELEASE_VERSION: ${{ needs.changes.outputs.version }}
|
|
448
|
+
WHEEL_BUILD: ${{ needs.changes.outputs.build }}
|
|
449
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
382
450
|
run: |
|
|
383
451
|
python - <<'PY'
|
|
384
452
|
from email.parser import BytesParser
|
|
453
|
+
from hashlib import sha256
|
|
385
454
|
from json import load
|
|
386
455
|
from os import environ
|
|
387
456
|
from pathlib import Path
|
|
457
|
+
from shutil import copy2
|
|
388
458
|
from urllib.error import HTTPError
|
|
389
459
|
from urllib.request import urlopen
|
|
390
460
|
from zipfile import ZipFile
|
|
391
461
|
|
|
392
462
|
wheels = list(Path('dist').glob('*.whl'))
|
|
393
463
|
sources = list(Path('dist').glob('*.tar.gz'))
|
|
394
|
-
|
|
395
|
-
|
|
464
|
+
expected_sources = 1 if environ['NEW_VERSION'] == 'true' else 0
|
|
465
|
+
if len(wheels) != 1 or len(sources) != expected_sources:
|
|
466
|
+
raise ValueError('Unexpected wheel/source distribution count for the release plan')
|
|
467
|
+
if wheels[0].name.split('-')[2] != environ['WHEEL_BUILD']:
|
|
468
|
+
raise ValueError('Wheel build tag does not match the release plan')
|
|
396
469
|
with ZipFile(wheels[0]) as archive:
|
|
397
470
|
metadata_path, = (name for name in archive.namelist() if name.endswith('.dist-info/METADATA'))
|
|
398
471
|
metadata = BytesParser().parsebytes(archive.read(metadata_path))
|
|
@@ -409,34 +482,42 @@ jobs:
|
|
|
409
482
|
except HTTPError as error:
|
|
410
483
|
if error.code != 404:
|
|
411
484
|
raise
|
|
412
|
-
|
|
485
|
+
published = {}
|
|
413
486
|
else:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
487
|
+
published = {file['filename']: file['digests']['sha256'] for file in release['urls']}
|
|
488
|
+
if environ['NEW_VERSION'] != 'true' and not published:
|
|
489
|
+
raise RuntimeError('Cannot rebuild a version that has not been published on PyPI')
|
|
490
|
+
upload = Path('upload')
|
|
491
|
+
upload.mkdir()
|
|
492
|
+
for path in [*wheels, *sources]:
|
|
493
|
+
if path.name in published:
|
|
494
|
+
if sha256(path.read_bytes()).hexdigest() != published[path.name]:
|
|
495
|
+
raise RuntimeError(f'Published file differs; refusing to overwrite {path.name}')
|
|
496
|
+
print(f'Already published: {path.name}')
|
|
497
|
+
else:
|
|
498
|
+
copy2(path, upload / path.name)
|
|
423
499
|
with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
|
|
424
|
-
output.write(f'publish={str(
|
|
500
|
+
output.write(f'publish={str(any(upload.iterdir())).lower()}\n')
|
|
425
501
|
output.write(f'version={version}\n')
|
|
426
502
|
PY
|
|
427
503
|
|
|
428
504
|
- name: Record verified release tag
|
|
429
505
|
env:
|
|
430
|
-
VERSION: ${{
|
|
506
|
+
VERSION: ${{ needs.changes.outputs.tag }}
|
|
507
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
431
508
|
run: |
|
|
432
509
|
tag="$VERSION"
|
|
433
510
|
cd release-source
|
|
434
511
|
if git show-ref --verify --quiet "refs/tags/$tag"; then
|
|
435
|
-
if [ "$(git rev-parse "$tag^{commit}")" != "$GITHUB_SHA" ]; then
|
|
512
|
+
if [ "$NEW_VERSION" = "true" ] && [ "$(git rev-parse "$tag^{commit}")" != "$GITHUB_SHA" ]; then
|
|
436
513
|
echo "::error::$tag already points at another commit."
|
|
437
514
|
exit 1
|
|
438
515
|
fi
|
|
439
516
|
else
|
|
517
|
+
if [ "$NEW_VERSION" != "true" ]; then
|
|
518
|
+
echo "::error::Existing release tag is missing: $tag"
|
|
519
|
+
exit 1
|
|
520
|
+
fi
|
|
440
521
|
git tag "$tag" "$GITHUB_SHA"
|
|
441
522
|
fi
|
|
442
523
|
git push origin "refs/tags/$tag"
|
|
@@ -455,29 +536,36 @@ jobs:
|
|
|
455
536
|
echo "::error::Configure PYPI_API_TOKEN in the pypi environment or repository Actions secrets."
|
|
456
537
|
exit 1
|
|
457
538
|
fi
|
|
458
|
-
python -m twine upload --non-interactive
|
|
539
|
+
python -m twine upload --non-interactive upload/*
|
|
459
540
|
|
|
460
541
|
- name: Publish GitHub Release with verified distributions
|
|
461
542
|
env:
|
|
462
543
|
GH_TOKEN: ${{ github.token }}
|
|
463
544
|
VERSION: ${{ steps.release.outputs.version }}
|
|
545
|
+
RELEASE_TAG: ${{ needs.changes.outputs.tag }}
|
|
546
|
+
WHEEL_BUILD: ${{ needs.changes.outputs.build }}
|
|
547
|
+
NEW_VERSION: ${{ needs.changes.outputs.new_version }}
|
|
464
548
|
run: |
|
|
465
549
|
python - <<'PY'
|
|
466
550
|
from json import load
|
|
551
|
+
from hashlib import sha256
|
|
467
552
|
from os import environ
|
|
468
553
|
from pathlib import Path
|
|
469
554
|
from subprocess import run
|
|
555
|
+
from tempfile import TemporaryDirectory
|
|
470
556
|
from urllib.error import HTTPError
|
|
471
557
|
from urllib.parse import quote
|
|
472
558
|
from urllib.request import Request, urlopen
|
|
473
559
|
|
|
474
560
|
repository = environ['GITHUB_REPOSITORY']
|
|
475
561
|
version = environ['VERSION']
|
|
562
|
+
tag = environ['RELEASE_TAG']
|
|
563
|
+
release = None
|
|
476
564
|
notes = Path('release-notes/README.md')
|
|
477
565
|
if not notes.read_text(encoding='utf-8').strip():
|
|
478
566
|
raise ValueError('Customer release notes are empty')
|
|
479
567
|
request = Request(
|
|
480
|
-
f'{environ["GITHUB_API_URL"]}/repos/{repository}/releases/tags/{quote(
|
|
568
|
+
f'{environ["GITHUB_API_URL"]}/repos/{repository}/releases/tags/{quote(tag, safe="")}',
|
|
481
569
|
headers={
|
|
482
570
|
'Authorization': f'Bearer {environ["GH_TOKEN"]}',
|
|
483
571
|
'Accept': 'application/vnd.github+json',
|
|
@@ -490,20 +578,35 @@ jobs:
|
|
|
490
578
|
except HTTPError as error:
|
|
491
579
|
if error.code != 404:
|
|
492
580
|
raise
|
|
581
|
+
if environ['NEW_VERSION'] != 'true':
|
|
582
|
+
raise RuntimeError('Existing GitHub Release is missing') from error
|
|
493
583
|
run([
|
|
494
|
-
'gh', 'release', 'create',
|
|
584
|
+
'gh', 'release', 'create', tag, '--repo', repository,
|
|
495
585
|
'--verify-tag', '--title', version, '--notes-file', str(notes), '--draft',
|
|
496
586
|
], check=True)
|
|
497
587
|
else:
|
|
498
|
-
if release['tag_name'] !=
|
|
588
|
+
if release['tag_name'] != tag:
|
|
499
589
|
raise ValueError('GitHub Release does not match the verified version')
|
|
590
|
+
if environ['NEW_VERSION'] != 'true':
|
|
591
|
+
if not (release['body'] or '').strip():
|
|
592
|
+
raise ValueError('Existing release notes are empty')
|
|
593
|
+
notes.write_text(release['body'], encoding='utf-8')
|
|
500
594
|
artifacts = sorted(str(path) for path in Path('dist').iterdir() if path.is_file())
|
|
595
|
+
existing = {asset['name'] for asset in release['assets']} if release is not None else set()
|
|
596
|
+
for artifact in artifacts:
|
|
597
|
+
path = Path(artifact)
|
|
598
|
+
if path.name in existing:
|
|
599
|
+
with TemporaryDirectory() as directory:
|
|
600
|
+
run([
|
|
601
|
+
'gh', 'release', 'download', tag, '--repo', repository,
|
|
602
|
+
'--pattern', path.name, '--dir', directory,
|
|
603
|
+
], check=True)
|
|
604
|
+
if sha256((Path(directory) / path.name).read_bytes()).digest() != sha256(path.read_bytes()).digest():
|
|
605
|
+
raise RuntimeError(f'GitHub asset differs; refusing to overwrite {path.name}')
|
|
606
|
+
else:
|
|
607
|
+
run(['gh', 'release', 'upload', tag, '--repo', repository, artifact], check=True)
|
|
501
608
|
run([
|
|
502
|
-
'gh', 'release', '
|
|
503
|
-
'--clobber', *artifacts,
|
|
504
|
-
], check=True)
|
|
505
|
-
run([
|
|
506
|
-
'gh', 'release', 'edit', version, '--repo', repository,
|
|
609
|
+
'gh', 'release', 'edit', tag, '--repo', repository,
|
|
507
610
|
'--notes-file', str(notes), '--draft=false',
|
|
508
611
|
], check=True)
|
|
509
612
|
PY
|
{defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4/DefStream_RabbitMQ.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream-RabbitMQ
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: RabbitMQ broker adapter for DefStream
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream-RabbitMQ
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: DefStream>=0.1.
|
|
19
|
+
Requires-Dist: DefStream>=0.1.7
|
|
20
20
|
Requires-Dist: pika>=1.3.2
|
|
21
21
|
Dynamic: author
|
|
22
22
|
Dynamic: classifier
|
|
@@ -187,6 +187,11 @@ Worker는 실행과 완료 훅이 끝난 뒤 ACK하고, 실패하면 지정한
|
|
|
187
187
|
|
|
188
188
|
## 워커 작업 우선순위
|
|
189
189
|
|
|
190
|
+
작업을 발행할 때는 `DefStream.Worker.emit` 또는
|
|
191
|
+
`connection.emit(body, parameters)`을 사용합니다. 기존 작업 발행 이름 `send`는
|
|
192
|
+
`emit`으로 바뀌었으므로 코어와 어댑터를 함께 갱신하세요.
|
|
193
|
+
큐·Exchange·Stream의 저수준 `send()`와 RPC `call()`은 변경하지 않습니다.
|
|
194
|
+
|
|
190
195
|
작업에 지정한 `Subscription`에 기본값 `priority`와 큐 최대값 `max_priority`를
|
|
191
196
|
함께 설정합니다. `Run`에는 별도의 `priority` 설정을 두지 않습니다.
|
|
192
197
|
|
|
@@ -205,7 +210,7 @@ class PriorityAdd(
|
|
|
205
210
|
|
|
206
211
|
- 워커 API `declare(task=PriorityAdd, broker=connection)`는 큐에
|
|
207
212
|
`x-max-priority=5`를 반영합니다. 기존 DLQ 설정도 보존합니다.
|
|
208
|
-
- `
|
|
213
|
+
- `emit(..., task=PriorityAdd, broker=connection)` 및
|
|
209
214
|
`request(..., task=PriorityAdd, broker=connection)`는 작업 기본값 `2`를 사용합니다.
|
|
210
215
|
호출에 `priority=0` 또는 다른 유효한 값을 지정하여 재정의할 수 있습니다.
|
|
211
216
|
- 기본값만 정의하면 큐 최대값은 `255`, 최대값만 정의하면 기본값은 `None`입니다.
|
{defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/DefStream_RabbitMQ.egg-info/SOURCES.txt
RENAMED
|
@@ -14,6 +14,9 @@ issues/feature/1/README.md
|
|
|
14
14
|
issues/feature/1/REQUIREMENTS.md
|
|
15
15
|
issues/feature/1/TASK.md
|
|
16
16
|
issues/feature/1/ci.refs.json
|
|
17
|
+
issues/fix/3/IMPLEMENTS.md
|
|
18
|
+
issues/fix/3/README.md
|
|
19
|
+
issues/fix/3/REQUIREMENTS.md
|
|
17
20
|
sample/Sample.RPC.py
|
|
18
21
|
sample/Sample.Stream.py
|
|
19
22
|
sample/Sample.py
|
|
@@ -32,5 +35,6 @@ test/test_broker_methods.py
|
|
|
32
35
|
test/test_priorities.py
|
|
33
36
|
test/test_rabbitmq_integration.py
|
|
34
37
|
test/test_rabbitmq_worker.py
|
|
38
|
+
test/test_release_workflow.py
|
|
35
39
|
test/test_rpc.py
|
|
36
40
|
test/test_rpc_integration.py
|
{defstream_rabbitmq-0.1.2.6/DefStream_RabbitMQ.egg-info → defstream_rabbitmq-0.1.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream-RabbitMQ
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: RabbitMQ broker adapter for DefStream
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream-RabbitMQ
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: DefStream>=0.1.
|
|
19
|
+
Requires-Dist: DefStream>=0.1.7
|
|
20
20
|
Requires-Dist: pika>=1.3.2
|
|
21
21
|
Dynamic: author
|
|
22
22
|
Dynamic: classifier
|
|
@@ -187,6 +187,11 @@ Worker는 실행과 완료 훅이 끝난 뒤 ACK하고, 실패하면 지정한
|
|
|
187
187
|
|
|
188
188
|
## 워커 작업 우선순위
|
|
189
189
|
|
|
190
|
+
작업을 발행할 때는 `DefStream.Worker.emit` 또는
|
|
191
|
+
`connection.emit(body, parameters)`을 사용합니다. 기존 작업 발행 이름 `send`는
|
|
192
|
+
`emit`으로 바뀌었으므로 코어와 어댑터를 함께 갱신하세요.
|
|
193
|
+
큐·Exchange·Stream의 저수준 `send()`와 RPC `call()`은 변경하지 않습니다.
|
|
194
|
+
|
|
190
195
|
작업에 지정한 `Subscription`에 기본값 `priority`와 큐 최대값 `max_priority`를
|
|
191
196
|
함께 설정합니다. `Run`에는 별도의 `priority` 설정을 두지 않습니다.
|
|
192
197
|
|
|
@@ -205,7 +210,7 @@ class PriorityAdd(
|
|
|
205
210
|
|
|
206
211
|
- 워커 API `declare(task=PriorityAdd, broker=connection)`는 큐에
|
|
207
212
|
`x-max-priority=5`를 반영합니다. 기존 DLQ 설정도 보존합니다.
|
|
208
|
-
- `
|
|
213
|
+
- `emit(..., task=PriorityAdd, broker=connection)` 및
|
|
209
214
|
`request(..., task=PriorityAdd, broker=connection)`는 작업 기본값 `2`를 사용합니다.
|
|
210
215
|
호출에 `priority=0` 또는 다른 유효한 값을 지정하여 재정의할 수 있습니다.
|
|
211
216
|
- 기본값만 정의하면 큐 최대값은 `255`, 최대값만 정의하면 기본값은 `None`입니다.
|
|
@@ -157,6 +157,11 @@ fail explicitly; queues are not automatically deleted and recreated.
|
|
|
157
157
|
|
|
158
158
|
## Worker task priorities
|
|
159
159
|
|
|
160
|
+
Publish tasks with `DefStream.Worker.emit` or `connection.emit(body, parameters)`.
|
|
161
|
+
The task publication API has been renamed from `send` to `emit`; update the core
|
|
162
|
+
and adapter together. Low-level `send()` and RPC `call()` on queues, exchanges,
|
|
163
|
+
and streams remain unchanged.
|
|
164
|
+
|
|
160
165
|
Set the default `priority` and queue `max_priority` together in the task's
|
|
161
166
|
`Subscription`. There is no separate `priority` setting on `Run`.
|
|
162
167
|
|
|
@@ -175,7 +180,7 @@ class PriorityAdd(
|
|
|
175
180
|
|
|
176
181
|
- The Worker API `declare(task=PriorityAdd, broker=connection)` declares the queue
|
|
177
182
|
with `x-max-priority=5` while preserving existing DLQ settings.
|
|
178
|
-
- `
|
|
183
|
+
- `emit(..., task=PriorityAdd, broker=connection)` and
|
|
179
184
|
`request(..., task=PriorityAdd, broker=connection)` use the task default of `2`.
|
|
180
185
|
Pass `priority=0` or another valid value to override it for an individual call.
|
|
181
186
|
- Setting only the default sets the queue maximum to `255`. Setting only the
|
|
@@ -156,6 +156,11 @@ Worker는 실행과 완료 훅이 끝난 뒤 ACK하고, 실패하면 지정한
|
|
|
156
156
|
|
|
157
157
|
## 워커 작업 우선순위
|
|
158
158
|
|
|
159
|
+
작업을 발행할 때는 `DefStream.Worker.emit` 또는
|
|
160
|
+
`connection.emit(body, parameters)`을 사용합니다. 기존 작업 발행 이름 `send`는
|
|
161
|
+
`emit`으로 바뀌었으므로 코어와 어댑터를 함께 갱신하세요.
|
|
162
|
+
큐·Exchange·Stream의 저수준 `send()`와 RPC `call()`은 변경하지 않습니다.
|
|
163
|
+
|
|
159
164
|
작업에 지정한 `Subscription`에 기본값 `priority`와 큐 최대값 `max_priority`를
|
|
160
165
|
함께 설정합니다. `Run`에는 별도의 `priority` 설정을 두지 않습니다.
|
|
161
166
|
|
|
@@ -174,7 +179,7 @@ class PriorityAdd(
|
|
|
174
179
|
|
|
175
180
|
- 워커 API `declare(task=PriorityAdd, broker=connection)`는 큐에
|
|
176
181
|
`x-max-priority=5`를 반영합니다. 기존 DLQ 설정도 보존합니다.
|
|
177
|
-
- `
|
|
182
|
+
- `emit(..., task=PriorityAdd, broker=connection)` 및
|
|
178
183
|
`request(..., task=PriorityAdd, broker=connection)`는 작업 기본값 `2`를 사용합니다.
|
|
179
184
|
호출에 `priority=0` 또는 다른 유효한 값을 지정하여 재정의할 수 있습니다.
|
|
180
185
|
- 기본값만 정의하면 큐 최대값은 `255`, 최대값만 정의하면 기본값은 `None`입니다.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 작업 발행 API 이름 통일 구현 기록
|
|
2
|
+
|
|
3
|
+
- 현재 브랜치 `master`를 기준으로 기록했다. 브랜치를 새로 만들거나 전환하지 않았다.
|
|
4
|
+
- [요구사항](./REQUIREMENTS.md)에 따라 `Connection.send`를 `emit`으로 변경했다.
|
|
5
|
+
- 공통 `PrioritySupport`가 `Emittable`·`Requestable`을 상속하므로 연결의
|
|
6
|
+
중복 상속을 제거했다. 두 기능은 계속 제공한다.
|
|
7
|
+
- 발행 확인·게시 채널 재사용·우선순위 검증·오류 전파와 RPC 동작을 유지했다.
|
|
8
|
+
- 테스트, 한영 README와 연결된 우선순위 사용 예제를 갱신했다.
|
|
9
|
+
- Docker `defstream-rabbitmq`의 RabbitMQ 3.13.7에서
|
|
10
|
+
`DEFSTREAM_RABBITMQ_TESTS=1`로 어댑터 전체 테스트 60개가 모두 통과했다.
|
|
11
|
+
- 현재 코어와 로컬 어댑터 소스를 직접 로드했으며 설치된 과거 패키지로 검증하지 않았다.
|
|
12
|
+
- 코어 164개 중 160개 통과·Redis 전용 4개 건너뜀,
|
|
13
|
+
Redis 어댑터 36개 중 32개 통과·실서버 전용 4개 건너뜀도 확인했다.
|
|
14
|
+
- 검증 후 테스트 컨테이너를 종료하고 보존했다.
|
|
15
|
+
|
|
16
|
+
## Redis 실서버를 포함한 전체 재검증
|
|
17
|
+
|
|
18
|
+
- 2026-10-19에 Redis 7.4.11의 `defstream-redis`와 기존 `defstream-rabbitmq`를
|
|
19
|
+
함께 실행하고 두 실서버 테스트 옵션을 모두 활성화했다.
|
|
20
|
+
- 현재 소스를 사용해 코어 164개, RabbitMQ 어댑터 60개, Redis 어댑터 36개,
|
|
21
|
+
총 260개 전부 통과했다. 건너뜀·실패·오류는 없다.
|
|
22
|
+
- 검증 후 두 테스트 컨테이너를 종료하고 재사용 가능하게 보존했다.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 작업 발행 API를 `emit`으로 통일
|
|
2
|
+
|
|
3
|
+
작업을 발행하는 연결 메서드와 코어 도우미의 이름이 `send`에서 `emit`으로 바뀌었습니다.
|
|
4
|
+
코어와 RabbitMQ 어댑터를 함께 갱신하세요. 이전 이름의 호환 별칭은 제공하지 않습니다.
|
|
5
|
+
|
|
6
|
+
- 작업 도우미는 `from DefStream.Worker import emit`으로 가져옵니다.
|
|
7
|
+
- 연결을 직접 사용했다면 `connection.send(body, parameters)`를
|
|
8
|
+
`connection.emit(body, parameters)`로 바꾸세요.
|
|
9
|
+
- 사용자 정의 연결의 작업 발행 계약은 `TaskSender` 대신 `Emittable`을 사용합니다.
|
|
10
|
+
- 우선순위, 발행 확인과 RPC 동작은 동일합니다.
|
|
11
|
+
- 저수준 `Queue.send`·`Exchange.send`·`Stream.send`와 RPC `call`은 그대로 사용할 수 있습니다.
|
|
12
|
+
|
|
13
|
+
연결은 기존처럼 호출자가 열고 닫습니다.
|
|
14
|
+
작업 정의를 사용하는 `emit(body, task=Add, broker=connection)`은
|
|
15
|
+
미리 선언한 큐에 작업을 발행하며 큐를 자동 선언하지 않습니다.
|
|
16
|
+
|
|
17
|
+
## 상세 기록
|
|
18
|
+
|
|
19
|
+
- [요구사항](./REQUIREMENTS.md)
|
|
20
|
+
- [구현 및 검증](./IMPLEMENTS.md)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# 작업 발행 API 이름 통일 요구사항
|
|
2
|
+
|
|
3
|
+
- 공통 코어의 `TaskSender`를 대신하는 `Emittable`을 구현한다.
|
|
4
|
+
- 작업 발행 `Connection.send(body, parameters, priority=...)`를 `emit`으로 변경한다.
|
|
5
|
+
기존 이름의 호환 별칭은 제공하지 않는다.
|
|
6
|
+
- 우선순위·발행 확인·채널 재사용·오류 전파 동작은 유지한다.
|
|
7
|
+
- 저수준 큐·Exchange·Stream의 `send`와 RPC `call`·`request`는 변경하지 않는다.
|
|
8
|
+
- 테스트, 예제와 한영 사용 설명서를 갱신하고 실제 Docker RabbitMQ에서 검증한다.
|
|
@@ -7,8 +7,6 @@ from DefStream.Broker import (
|
|
|
7
7
|
GetStream,
|
|
8
8
|
GetConsumer,
|
|
9
9
|
Declarable,
|
|
10
|
-
TaskSender,
|
|
11
|
-
Requestable,
|
|
12
10
|
PrioritySupport,
|
|
13
11
|
EventHandler,
|
|
14
12
|
)
|
|
@@ -108,8 +106,6 @@ class Connection(
|
|
|
108
106
|
GetStream,
|
|
109
107
|
GetConsumer,
|
|
110
108
|
Declarable,
|
|
111
|
-
TaskSender,
|
|
112
|
-
Requestable,
|
|
113
109
|
PrioritySupport,
|
|
114
110
|
):
|
|
115
111
|
"""Connection of Event Broker for RabbitMQ"""
|
|
@@ -179,7 +175,7 @@ class Connection(
|
|
|
179
175
|
"""작업 기본값을 검증하고 선언·발행에 사용할 구독 설정을 반환한다."""
|
|
180
176
|
return Subscription.parse(parameters)
|
|
181
177
|
|
|
182
|
-
def
|
|
178
|
+
def emit(
|
|
183
179
|
self,
|
|
184
180
|
body: AnyValue,
|
|
185
181
|
parameters: AnyValue,
|
|
@@ -26,7 +26,7 @@ from DefStream.Broker.Implements.RabbitMQ import (
|
|
|
26
26
|
from DefStream.Worker import (
|
|
27
27
|
Run,
|
|
28
28
|
declare,
|
|
29
|
-
|
|
29
|
+
emit,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
@@ -118,7 +118,7 @@ class PriorityTests(TestCase):
|
|
|
118
118
|
'max_priority': 5,
|
|
119
119
|
'priority': 3,
|
|
120
120
|
}
|
|
121
|
-
broker.
|
|
121
|
+
broker.emit(
|
|
122
122
|
{},
|
|
123
123
|
settings,
|
|
124
124
|
priority=override,
|
|
@@ -387,7 +387,7 @@ class PriorityIntegrationTests(TestCase):
|
|
|
387
387
|
3,
|
|
388
388
|
),
|
|
389
389
|
):
|
|
390
|
-
|
|
390
|
+
emit(
|
|
391
391
|
{'label': label},
|
|
392
392
|
task=Task,
|
|
393
393
|
broker=broker,
|
|
@@ -408,7 +408,7 @@ class PriorityIntegrationTests(TestCase):
|
|
|
408
408
|
label,
|
|
409
409
|
)
|
|
410
410
|
event.ack()
|
|
411
|
-
|
|
411
|
+
emit(
|
|
412
412
|
{'label': 'retry'},
|
|
413
413
|
task=Task,
|
|
414
414
|
broker=broker,
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
from contextlib import (
|
|
2
|
+
closing,
|
|
3
|
+
redirect_stdout,
|
|
4
|
+
)
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
from io import (
|
|
7
|
+
BytesIO,
|
|
8
|
+
StringIO,
|
|
9
|
+
)
|
|
10
|
+
from json import dumps
|
|
11
|
+
from os import environ
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from subprocess import check_output
|
|
14
|
+
from tarfile import (
|
|
15
|
+
TarInfo,
|
|
16
|
+
open as open_tar,
|
|
17
|
+
)
|
|
18
|
+
from tempfile import TemporaryDirectory
|
|
19
|
+
from unittest import (
|
|
20
|
+
TestCase,
|
|
21
|
+
main,
|
|
22
|
+
)
|
|
23
|
+
from unittest.mock import patch
|
|
24
|
+
from urllib.error import HTTPError
|
|
25
|
+
from zipfile import ZipFile
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
WORKFLOW = Path(__file__).resolve().parents[1] / '.github' / 'workflows' / 'ci.yml'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def workflow_script(name: str) -> str:
|
|
32
|
+
lines = WORKFLOW.read_text(encoding='utf-8').splitlines()
|
|
33
|
+
start = lines.index(f' - name: {name}')
|
|
34
|
+
start = lines.index(" python - <<'PY'", start) + 1
|
|
35
|
+
end = lines.index(' PY', start)
|
|
36
|
+
return '\n'.join(line[10:] for line in lines[start:end])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReleaseWorkflowTests(TestCase):
|
|
40
|
+
def test_embedded_python_syntax(self) -> None:
|
|
41
|
+
lines = WORKFLOW.read_text(encoding='utf-8').splitlines()
|
|
42
|
+
for index, line in enumerate(lines):
|
|
43
|
+
if line == " python - <<'PY'":
|
|
44
|
+
end = lines.index(' PY', index + 1)
|
|
45
|
+
code = '\n'.join(line[10:] for line in lines[index + 1:end])
|
|
46
|
+
compile(code, str(WORKFLOW), 'exec')
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
def plan(
|
|
50
|
+
self,
|
|
51
|
+
tags: str,
|
|
52
|
+
changed: str = '',
|
|
53
|
+
event: str = 'true',
|
|
54
|
+
) -> dict:
|
|
55
|
+
with TemporaryDirectory() as directory:
|
|
56
|
+
output = Path(directory) / 'output'
|
|
57
|
+
event_path = Path(directory) / 'event.json'
|
|
58
|
+
event_path.write_text(dumps({'pull_request': {'base': {'sha': 'pr-base'}}}))
|
|
59
|
+
environment = {
|
|
60
|
+
'GITHUB_OUTPUT': str(output),
|
|
61
|
+
'RELEASE_PUSH': event,
|
|
62
|
+
'RELEASE_SERIES': '0.1',
|
|
63
|
+
'BEFORE': 'before',
|
|
64
|
+
'GITHUB_SHA': 'head',
|
|
65
|
+
'GITHUB_RUN_NUMBER': '42',
|
|
66
|
+
'GITHUB_EVENT_PATH': str(event_path),
|
|
67
|
+
}
|
|
68
|
+
with (
|
|
69
|
+
patch.dict(environ, environment),
|
|
70
|
+
patch('subprocess.check_output', side_effect=[tags, changed]) as git,
|
|
71
|
+
redirect_stdout(StringIO()),
|
|
72
|
+
):
|
|
73
|
+
try:
|
|
74
|
+
exec(workflow_script('Check complete push range and plan version'), {})
|
|
75
|
+
except SystemExit as error:
|
|
76
|
+
self.assertEqual(error.code, 0)
|
|
77
|
+
self.commands = git.call_args_list
|
|
78
|
+
return dict(line.split('=', 1) for line in output.read_text().splitlines())
|
|
79
|
+
|
|
80
|
+
def test_code_and_packaging_changes_increase_patch(self) -> None:
|
|
81
|
+
for path in ['src/__init__.py', 'src/Worker/__init__.py', 'setup.py', 'pyproject.toml']:
|
|
82
|
+
with self.subTest(path=path):
|
|
83
|
+
result = self.plan('0.1.4', path)
|
|
84
|
+
self.assertEqual(result['version'], '0.1.5')
|
|
85
|
+
self.assertEqual(result['new_version'], 'true')
|
|
86
|
+
self.assertEqual(result['build'], '42')
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def test_rebuild_keeps_public_version(self) -> None:
|
|
90
|
+
result = self.plan('0.1.3.7\n0.1.4')
|
|
91
|
+
self.assertEqual(result['version'], '0.1.4')
|
|
92
|
+
self.assertEqual(result['new_version'], 'false')
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
def test_migrating_legacy_versions_increases_patch(self) -> None:
|
|
96
|
+
for tag in ['0.1.4.12', 'v0.1.4.12', '0.1.4.post1', '0.1.4.0', '0.1.4.post0']:
|
|
97
|
+
with self.subTest(tag=tag):
|
|
98
|
+
result = self.plan(tag)
|
|
99
|
+
self.assertEqual(result['version'], '0.1.5')
|
|
100
|
+
self.assertEqual(result['new_version'], 'true')
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
def test_rebuild_preserves_existing_tag(self) -> None:
|
|
104
|
+
self.assertEqual(self.plan('v0.1.4')['tag'], 'v0.1.4')
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def test_source_comparison_includes_previously_failed_pushes(self) -> None:
|
|
108
|
+
self.plan('0.1.4', 'setup.py')
|
|
109
|
+
self.assertIn('0.1.4', self.commands[1].args[0])
|
|
110
|
+
self.assertNotIn('before', self.commands[1].args[0])
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
def test_initial_release(self) -> None:
|
|
114
|
+
self.assertEqual(self.plan('', 'src/__init__.py')['version'], '0.1.0')
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
def test_rebuild_without_release_history_is_rejected(self) -> None:
|
|
118
|
+
with self.assertRaisesRegex(ValueError, 'No release tag'):
|
|
119
|
+
self.plan('')
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def test_pull_request_does_not_release(self) -> None:
|
|
123
|
+
result = self.plan('0.1.4', event='false')
|
|
124
|
+
self.assertEqual(result['release'], 'false')
|
|
125
|
+
self.assertEqual(result['new_version'], 'false')
|
|
126
|
+
self.assertEqual(result['tag'], '0.1.4')
|
|
127
|
+
self.assertIn('pr-base', self.commands[0].args[0])
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
def test_git_pathspec_distinguishes_resources_and_packaging(self) -> None:
|
|
131
|
+
with TemporaryDirectory() as directory:
|
|
132
|
+
root = Path(directory)
|
|
133
|
+
check_output(['git', 'init', '--quiet', str(root)])
|
|
134
|
+
names = [
|
|
135
|
+
'src/__init__.py',
|
|
136
|
+
'src/nested/code.py',
|
|
137
|
+
'src/nested/view.html',
|
|
138
|
+
'setup.py',
|
|
139
|
+
'setup.cfg',
|
|
140
|
+
'pyproject.toml',
|
|
141
|
+
'MANIFEST.in',
|
|
142
|
+
'README.md',
|
|
143
|
+
'.github/workflows/ci.yml',
|
|
144
|
+
]
|
|
145
|
+
for name in names:
|
|
146
|
+
path = root / name
|
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
path.write_text('test\n')
|
|
149
|
+
check_output(['git', '-C', str(root), 'add', '.'])
|
|
150
|
+
check_output([
|
|
151
|
+
'git', '-C', str(root),
|
|
152
|
+
'-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
|
|
153
|
+
'commit', '--quiet', '-m', 'test',
|
|
154
|
+
])
|
|
155
|
+
base = check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
|
|
156
|
+
for name in names:
|
|
157
|
+
(root / name).write_text('changed\n')
|
|
158
|
+
self.plan('0.1.4')
|
|
159
|
+
diff = self.commands[1].args[0]
|
|
160
|
+
selected = check_output(
|
|
161
|
+
['git', '-C', str(root), 'diff', '--name-only', base, '--', *diff[diff.index('--') + 1:]],
|
|
162
|
+
text=True,
|
|
163
|
+
).splitlines()
|
|
164
|
+
self.assertEqual(set(selected), set(names[:2] + names[3:7]))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
def test_direct_push_generates_notes(self) -> None:
|
|
168
|
+
with TemporaryDirectory() as directory:
|
|
169
|
+
output = Path(directory) / 'output'
|
|
170
|
+
environment = {
|
|
171
|
+
'GITHUB_OUTPUT': str(output),
|
|
172
|
+
'GITHUB_SHA': 'head',
|
|
173
|
+
'GITHUB_EVENT_NAME': 'push',
|
|
174
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
175
|
+
'BEFORE': 'before',
|
|
176
|
+
'RUNNER_TEMP': directory,
|
|
177
|
+
'NEW_VERSION': 'true',
|
|
178
|
+
}
|
|
179
|
+
with (
|
|
180
|
+
patch.dict(environ, environment),
|
|
181
|
+
patch('subprocess.check_output', side_effect=[b'[[]]', b'- setup.py update (abc)\n']),
|
|
182
|
+
redirect_stdout(StringIO()),
|
|
183
|
+
):
|
|
184
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
185
|
+
self.assertEqual(output.read_text().strip(), 'branch=master')
|
|
186
|
+
notes = (Path(directory) / 'release-notes' / 'README.md').read_text(encoding='utf-8')
|
|
187
|
+
self.assertIn('setup.py update', notes)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
def test_pr_still_requires_changelog(self) -> None:
|
|
191
|
+
with TemporaryDirectory() as directory:
|
|
192
|
+
event = Path(directory) / 'event.json'
|
|
193
|
+
event.write_text(dumps({'pull_request': {'head': {'ref': 'fix/1'}}}))
|
|
194
|
+
with (
|
|
195
|
+
patch.dict(environ, {
|
|
196
|
+
'GITHUB_EVENT_NAME': 'pull_request',
|
|
197
|
+
'GITHUB_EVENT_PATH': str(event),
|
|
198
|
+
'GITHUB_SHA': 'head',
|
|
199
|
+
'NEW_VERSION': 'true',
|
|
200
|
+
}),
|
|
201
|
+
patch('subprocess.check_output', return_value=b''),
|
|
202
|
+
):
|
|
203
|
+
with self.assertRaisesRegex(ValueError, 'Changelog must exist'):
|
|
204
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
def check_upload(
|
|
208
|
+
self,
|
|
209
|
+
published: dict,
|
|
210
|
+
new_version: bool = False,
|
|
211
|
+
) -> tuple:
|
|
212
|
+
with TemporaryDirectory() as directory:
|
|
213
|
+
root = Path(directory)
|
|
214
|
+
dist = root / 'dist'
|
|
215
|
+
dist.mkdir()
|
|
216
|
+
wheel = dist / 'defstream-0.1.5-42-py3-none-any.whl'
|
|
217
|
+
with ZipFile(wheel, 'w') as archive:
|
|
218
|
+
archive.writestr('defstream-0.1.5.dist-info/METADATA', 'Name: DefStream\nVersion: 0.1.5\n')
|
|
219
|
+
if new_version:
|
|
220
|
+
(dist / 'defstream-0.1.5.tar.gz').write_bytes(b'sdist')
|
|
221
|
+
wheel_hash = sha256(wheel.read_bytes()).hexdigest()
|
|
222
|
+
files = {
|
|
223
|
+
name: wheel_hash if digest == 'matching' else digest
|
|
224
|
+
for name, digest in published.items()
|
|
225
|
+
}
|
|
226
|
+
response = StringIO(dumps({
|
|
227
|
+
'urls': [
|
|
228
|
+
{'filename': name, 'digests': {'sha256': digest}}
|
|
229
|
+
for name, digest in files.items()
|
|
230
|
+
],
|
|
231
|
+
}))
|
|
232
|
+
if not published:
|
|
233
|
+
response.close()
|
|
234
|
+
response = HTTPError('https://example.invalid', 404, 'not found', None, BytesIO())
|
|
235
|
+
with (
|
|
236
|
+
closing(response),
|
|
237
|
+
patch.dict(environ, {
|
|
238
|
+
'PROJECT': 'DefStream',
|
|
239
|
+
'RELEASE_VERSION': '0.1.5',
|
|
240
|
+
'WHEEL_BUILD': '42',
|
|
241
|
+
'NEW_VERSION': str(new_version).lower(),
|
|
242
|
+
'GITHUB_OUTPUT': str(root / 'output'),
|
|
243
|
+
}),
|
|
244
|
+
patch('pathlib.Path', side_effect=lambda *args: root.joinpath(*args)),
|
|
245
|
+
patch('urllib.request.urlopen', side_effect=[response]),
|
|
246
|
+
redirect_stdout(StringIO()),
|
|
247
|
+
):
|
|
248
|
+
exec(workflow_script('Check release status'), {})
|
|
249
|
+
result = dict(line.split('=', 1) for line in (root / 'output').read_text().splitlines())
|
|
250
|
+
return result, {path.name for path in (root / 'upload').iterdir()}
|
|
251
|
+
|
|
252
|
+
def test_new_release_uploads_wheel_and_sdist(self) -> None:
|
|
253
|
+
result, files = self.check_upload({}, new_version=True)
|
|
254
|
+
self.assertEqual(result['publish'], 'true')
|
|
255
|
+
self.assertEqual(files, {
|
|
256
|
+
'defstream-0.1.5-42-py3-none-any.whl',
|
|
257
|
+
'defstream-0.1.5.tar.gz',
|
|
258
|
+
})
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
def test_missing_pypi_release_does_not_enter_http_error(self) -> None:
|
|
262
|
+
with patch.object(HTTPError, '__enter__', side_effect=AttributeError('fp is None')) as enter:
|
|
263
|
+
result, files = self.check_upload({}, new_version=True)
|
|
264
|
+
enter.assert_not_called()
|
|
265
|
+
self.assertEqual(result['publish'], 'true')
|
|
266
|
+
self.assertEqual(len(files), 2)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
def test_rebuild_only_uploads_new_wheel(self) -> None:
|
|
270
|
+
result, files = self.check_upload({'defstream-0.1.5.tar.gz': 'old-sdist-hash'})
|
|
271
|
+
self.assertEqual(result['publish'], 'true')
|
|
272
|
+
self.assertEqual(files, {'defstream-0.1.5-42-py3-none-any.whl'})
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
def test_rerun_skips_identical_published_wheel(self) -> None:
|
|
276
|
+
result, files = self.check_upload({'defstream-0.1.5-42-py3-none-any.whl': 'matching'})
|
|
277
|
+
self.assertEqual(result['publish'], 'false')
|
|
278
|
+
self.assertEqual(files, set())
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
def test_rerun_rejects_different_published_wheel(self) -> None:
|
|
282
|
+
with self.assertRaisesRegex(RuntimeError, 'refusing to overwrite'):
|
|
283
|
+
self.check_upload({'defstream-0.1.5-42-py3-none-any.whl': 'different'})
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
def test_rebuild_requires_existing_pypi_release(self) -> None:
|
|
287
|
+
with self.assertRaisesRegex(RuntimeError, 'has not been published'):
|
|
288
|
+
self.check_upload({})
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
def test_github_rebuild_preserves_original_notes(self) -> None:
|
|
292
|
+
with TemporaryDirectory() as directory:
|
|
293
|
+
root = Path(directory)
|
|
294
|
+
notes = root / 'release-notes' / 'README.md'
|
|
295
|
+
notes.parent.mkdir()
|
|
296
|
+
notes.write_text('new build notes', encoding='utf-8')
|
|
297
|
+
(root / 'dist').mkdir()
|
|
298
|
+
(root / 'dist' / 'defstream-0.1.5-42-py3-none-any.whl').write_bytes(b'wheel')
|
|
299
|
+
release = {'tag_name': '0.1.5', 'body': 'original release notes', 'assets': []}
|
|
300
|
+
for attempt in range(2):
|
|
301
|
+
with (
|
|
302
|
+
patch.dict(environ, {
|
|
303
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
304
|
+
'GITHUB_API_URL': 'https://api.github.com',
|
|
305
|
+
'GITHUB_SERVER_URL': 'https://github.com',
|
|
306
|
+
'GITHUB_SHA': 'source-commit',
|
|
307
|
+
'GH_TOKEN': 'test-token',
|
|
308
|
+
'VERSION': '0.1.5',
|
|
309
|
+
'RELEASE_TAG': '0.1.5',
|
|
310
|
+
'WHEEL_BUILD': '42',
|
|
311
|
+
'NEW_VERSION': 'false',
|
|
312
|
+
}),
|
|
313
|
+
patch('pathlib.Path', side_effect=lambda *args: root.joinpath(*args)),
|
|
314
|
+
patch('urllib.request.urlopen', return_value=StringIO(dumps(release))),
|
|
315
|
+
patch('subprocess.run') as commands,
|
|
316
|
+
):
|
|
317
|
+
exec(workflow_script('Publish GitHub Release with verified distributions'), {})
|
|
318
|
+
body = notes.read_text(encoding='utf-8')
|
|
319
|
+
self.assertIn('original release notes', body)
|
|
320
|
+
self.assertEqual(body, 'original release notes')
|
|
321
|
+
self.assertTrue(all('--clobber' not in call.args[0] for call in commands.call_args_list))
|
|
322
|
+
release['body'] = body
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
def test_rebuild_reuses_existing_notes_without_branch_changelog(self) -> None:
|
|
326
|
+
for event_name in ['push', 'pull_request']:
|
|
327
|
+
with self.subTest(event=event_name), TemporaryDirectory() as directory:
|
|
328
|
+
root = Path(directory)
|
|
329
|
+
event = root / 'event.json'
|
|
330
|
+
event.write_text(dumps({'pull_request': {'head': {'ref': 'docs/1'}}}))
|
|
331
|
+
body = '## 이전 변경로그\n\n기존 내용을 그대로 유지합니다.\n'
|
|
332
|
+
release = {'tag_name': '0.1.4', 'body': body}
|
|
333
|
+
responses = [dumps(release).encode('utf-8')]
|
|
334
|
+
if event_name == 'push':
|
|
335
|
+
responses.insert(0, b'[[]]')
|
|
336
|
+
with (
|
|
337
|
+
patch.dict(environ, {
|
|
338
|
+
'GITHUB_EVENT_NAME': event_name,
|
|
339
|
+
'GITHUB_EVENT_PATH': str(event),
|
|
340
|
+
'GITHUB_SHA': 'head',
|
|
341
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
342
|
+
'NEW_VERSION': 'false',
|
|
343
|
+
'RELEASE_TAG': '0.1.4',
|
|
344
|
+
'GITHUB_OUTPUT': str(root / 'output'),
|
|
345
|
+
'RUNNER_TEMP': directory,
|
|
346
|
+
}),
|
|
347
|
+
patch('subprocess.check_output', side_effect=responses) as commands,
|
|
348
|
+
redirect_stdout(StringIO()),
|
|
349
|
+
):
|
|
350
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
351
|
+
self.assertEqual((root / 'release-notes' / 'README.md').read_bytes(), body.encode('utf-8'))
|
|
352
|
+
self.assertTrue(all(call.args[0][0] == 'gh' for call in commands.call_args_list))
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
def test_rebuild_rejects_empty_existing_notes(self) -> None:
|
|
356
|
+
with TemporaryDirectory() as directory:
|
|
357
|
+
event = Path(directory) / 'event.json'
|
|
358
|
+
event.write_text(dumps({'pull_request': {'head': {'ref': 'docs/1'}}}))
|
|
359
|
+
with (
|
|
360
|
+
patch.dict(environ, {
|
|
361
|
+
'GITHUB_EVENT_NAME': 'pull_request',
|
|
362
|
+
'GITHUB_EVENT_PATH': str(event),
|
|
363
|
+
'GITHUB_SHA': 'head',
|
|
364
|
+
'GITHUB_REPOSITORY': 'owner/project',
|
|
365
|
+
'NEW_VERSION': 'false',
|
|
366
|
+
'RELEASE_TAG': '0.1.4',
|
|
367
|
+
}),
|
|
368
|
+
patch('subprocess.check_output', return_value=b'{"tag_name":"0.1.4","body":""}'),
|
|
369
|
+
):
|
|
370
|
+
with self.assertRaisesRegex(ValueError, 'missing or empty'):
|
|
371
|
+
exec(workflow_script('Validate PR branch changelog'), {})
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
def test_sdist_normalization_preserves_content_and_is_reproducible(self) -> None:
|
|
375
|
+
with TemporaryDirectory() as directory:
|
|
376
|
+
root = Path(directory)
|
|
377
|
+
dist = root / 'dist'
|
|
378
|
+
dist.mkdir()
|
|
379
|
+
for number in range(2):
|
|
380
|
+
with open_tar(dist / f'{number}.tar.gz', 'w:gz') as archive:
|
|
381
|
+
member = TarInfo('defstream-0.1.5/src/__init__.py')
|
|
382
|
+
member.mtime = 1700000000 + number
|
|
383
|
+
member.size = 4
|
|
384
|
+
member.pax_headers = {'mtime': str(member.mtime)}
|
|
385
|
+
archive.addfile(member, BytesIO(b'code'))
|
|
386
|
+
with (
|
|
387
|
+
patch.dict(environ, {'SOURCE_DATE_EPOCH': '1700000000'}),
|
|
388
|
+
patch('pathlib.Path', side_effect=lambda *args: root.joinpath(*args)),
|
|
389
|
+
):
|
|
390
|
+
exec(workflow_script('Normalize source archive timestamps'), {})
|
|
391
|
+
self.assertEqual((dist / '0.tar.gz').read_bytes(), (dist / '1.tar.gz').read_bytes())
|
|
392
|
+
with open_tar(dist / '0.tar.gz', 'r:gz') as archive:
|
|
393
|
+
self.assertEqual(archive.extractfile(archive.getmembers()[0]).read(), b'code')
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
if __name__ == '__main__':
|
|
398
|
+
main()
|
|
File without changes
|
|
File without changes
|
{defstream_rabbitmq-0.1.2.6 → defstream_rabbitmq-0.1.4}/DefStream_RabbitMQ.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|