DefStream 0.1.9__tar.gz → 0.1.10__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-0.1.9 → defstream-0.1.10}/DefStream.egg-info/PKG-INFO +5 -2
- {defstream-0.1.9 → defstream-0.1.10}/DefStream.egg-info/SOURCES.txt +3 -0
- {defstream-0.1.9 → defstream-0.1.10}/PKG-INFO +5 -2
- {defstream-0.1.9 → defstream-0.1.10}/README.en.md +4 -1
- {defstream-0.1.9 → defstream-0.1.10}/README.md +4 -1
- defstream-0.1.10/issues/fix/10/IMPLEMENTS.md +135 -0
- defstream-0.1.10/issues/fix/10/README.md +42 -0
- defstream-0.1.10/issues/fix/10/REQUIREMENTS.md +88 -0
- {defstream-0.1.9 → defstream-0.1.10}/sample/Worker/RabbitMQ/Tasks.py +4 -4
- {defstream-0.1.9 → defstream-0.1.10}/sample/Worker/Redis/Sample.py +4 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/Metrics.py +6 -4
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/Specifications.py +7 -6
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Run.py +44 -1
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Specification.py +0 -4
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Worker.py +6 -1
- {defstream-0.1.9 → defstream-0.1.10}/test/test_dashboard.py +1 -1
- {defstream-0.1.9 → defstream-0.1.10}/test/test_inspector.py +57 -3
- {defstream-0.1.9 → defstream-0.1.10}/test/test_specifications.py +118 -19
- {defstream-0.1.9 → defstream-0.1.10}/test/test_worker.py +49 -0
- {defstream-0.1.9 → defstream-0.1.10}/.github/workflows/ci.yml +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/.gitignore +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/DefStream.egg-info/dependency_links.txt +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/DefStream.egg-info/requires.txt +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/DefStream.egg-info/top_level.txt +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/LICENSE +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/1/IMPLEMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/1/README.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/1/REQUIREMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/1/TASK.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/1/ci.refs.json +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/8/IMPLEMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/8/README.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/8/REQUIREMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/feature/8/ci.refs.json +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/3/IMPLEMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/3/README.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/3/REQUIREMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/3/ci.refs.json +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/6/IMPLEMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/6/README.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/issues/fix/6/REQUIREMENTS.md +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/sample/Broker/Sample.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/sample/Worker/RabbitMQ/Client.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/sample/Worker/RabbitMQ/Worker.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/sample/Worker/Sample.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/setup.cfg +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/setup.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Configuration.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Connection.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Consumer.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Errors.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Event.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Implements/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Policies.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Priority.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/Protocol.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Broker/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Descriptor/DataObject.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Descriptor/Types.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Descriptor/Value.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Descriptor/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extension.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/Console.html +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/Console.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/Dashboard.html +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/Dashboard.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/HealthCheck.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Extensions/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Inspector.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/Response.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Inspector/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Pattern.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Array.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Common.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Compare.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Condition.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/DataObject.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/DateTime.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Number.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Object.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Size.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/String.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/Type.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Patterns/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Validate.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/Validator.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Validator/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Executor.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Message.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Pool.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/Startup.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/Worker/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/src/__init__.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_dashboard_ui.cjs +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_descriptor.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_inspector_console.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_inspector_logging.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_lifecycle_hooks.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_logging.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_pool_callbacks.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_process_signals.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_protocol_contracts.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_release_workflow.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_runner_hooks.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_task_messages.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_task_priorities.py +0 -0
- {defstream-0.1.9 → defstream-0.1.10}/test/test_worker_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: Synchronous message processing with pluggable broker adapters
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -545,13 +545,14 @@ class Add(
|
|
|
545
545
|
schema=request_schema,
|
|
546
546
|
validator=request_validator,
|
|
547
547
|
example={'a': 10, 'b': 20},
|
|
548
|
-
description='Add two integers',
|
|
549
548
|
),
|
|
550
549
|
response=Specification(
|
|
551
550
|
schema=response_schema,
|
|
552
551
|
validator=response_validator,
|
|
553
552
|
example=30,
|
|
554
553
|
),
|
|
554
|
+
name='add',
|
|
555
|
+
description='Add two integers',
|
|
555
556
|
):
|
|
556
557
|
def run(
|
|
557
558
|
self,
|
|
@@ -568,6 +569,8 @@ class Add(
|
|
|
568
569
|
- 일반 JSON Schema 딕셔너리와 직접 작성한 검증 함수도 각각 `schema`, `validator`에 전달할 수 있습니다.
|
|
569
570
|
- `example`은 문서용 값이며 자동 실행·검증하지 않습니다.
|
|
570
571
|
- `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
|
|
572
|
+
- Run의 `name`은 AsyncAPI Operation 키와 Metrics 작업 키에 함께 사용합니다.
|
|
573
|
+
생략하면 `모듈명.__qualname__`을 사용하며, Run의 `description`은 AsyncAPI Operation에 표시됩니다.
|
|
571
574
|
- 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
|
|
572
575
|
|
|
573
576
|
검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
|
|
@@ -18,6 +18,9 @@ issues/feature/8/IMPLEMENTS.md
|
|
|
18
18
|
issues/feature/8/README.md
|
|
19
19
|
issues/feature/8/REQUIREMENTS.md
|
|
20
20
|
issues/feature/8/ci.refs.json
|
|
21
|
+
issues/fix/10/IMPLEMENTS.md
|
|
22
|
+
issues/fix/10/README.md
|
|
23
|
+
issues/fix/10/REQUIREMENTS.md
|
|
21
24
|
issues/fix/3/IMPLEMENTS.md
|
|
22
25
|
issues/fix/3/README.md
|
|
23
26
|
issues/fix/3/REQUIREMENTS.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
4
4
|
Summary: Synchronous message processing with pluggable broker adapters
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -545,13 +545,14 @@ class Add(
|
|
|
545
545
|
schema=request_schema,
|
|
546
546
|
validator=request_validator,
|
|
547
547
|
example={'a': 10, 'b': 20},
|
|
548
|
-
description='Add two integers',
|
|
549
548
|
),
|
|
550
549
|
response=Specification(
|
|
551
550
|
schema=response_schema,
|
|
552
551
|
validator=response_validator,
|
|
553
552
|
example=30,
|
|
554
553
|
),
|
|
554
|
+
name='add',
|
|
555
|
+
description='Add two integers',
|
|
555
556
|
):
|
|
556
557
|
def run(
|
|
557
558
|
self,
|
|
@@ -568,6 +569,8 @@ class Add(
|
|
|
568
569
|
- 일반 JSON Schema 딕셔너리와 직접 작성한 검증 함수도 각각 `schema`, `validator`에 전달할 수 있습니다.
|
|
569
570
|
- `example`은 문서용 값이며 자동 실행·검증하지 않습니다.
|
|
570
571
|
- `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
|
|
572
|
+
- Run의 `name`은 AsyncAPI Operation 키와 Metrics 작업 키에 함께 사용합니다.
|
|
573
|
+
생략하면 `모듈명.__qualname__`을 사용하며, Run의 `description`은 AsyncAPI Operation에 표시됩니다.
|
|
571
574
|
- 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
|
|
572
575
|
|
|
573
576
|
검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
|
|
@@ -481,13 +481,14 @@ class Add(
|
|
|
481
481
|
schema=request_schema,
|
|
482
482
|
validator=request_validator,
|
|
483
483
|
example={'a': 10, 'b': 20},
|
|
484
|
-
description='Add two integers',
|
|
485
484
|
),
|
|
486
485
|
response=Specification(
|
|
487
486
|
schema=response_schema,
|
|
488
487
|
validator=response_validator,
|
|
489
488
|
example=30,
|
|
490
489
|
),
|
|
490
|
+
name='add',
|
|
491
|
+
description='Add two integers',
|
|
491
492
|
):
|
|
492
493
|
def run(
|
|
493
494
|
self,
|
|
@@ -504,6 +505,8 @@ class Add(
|
|
|
504
505
|
- Plain JSON Schema dictionaries and custom validation functions can also be passed as `schema` and `validator`.
|
|
505
506
|
- `example` is documentation data and is not automatically executed or validated.
|
|
506
507
|
- `format` defaults to `application/json`. It changes documented media types, not broker codecs.
|
|
508
|
+
- Run's `name` is used as both the AsyncAPI Operation key and the Metrics task key.
|
|
509
|
+
When omitted, `module.__qualname__` is used; Run's `description` appears on the AsyncAPI Operation.
|
|
507
510
|
- Execution order: setup hooks → request validation → `run()` → response validation → completion hooks → reply/ACK.
|
|
508
511
|
|
|
509
512
|
Validation exceptions are task failures and follow error hooks and the `on_error`
|
|
@@ -512,13 +512,14 @@ class Add(
|
|
|
512
512
|
schema=request_schema,
|
|
513
513
|
validator=request_validator,
|
|
514
514
|
example={'a': 10, 'b': 20},
|
|
515
|
-
description='Add two integers',
|
|
516
515
|
),
|
|
517
516
|
response=Specification(
|
|
518
517
|
schema=response_schema,
|
|
519
518
|
validator=response_validator,
|
|
520
519
|
example=30,
|
|
521
520
|
),
|
|
521
|
+
name='add',
|
|
522
|
+
description='Add two integers',
|
|
522
523
|
):
|
|
523
524
|
def run(
|
|
524
525
|
self,
|
|
@@ -535,6 +536,8 @@ class Add(
|
|
|
535
536
|
- 일반 JSON Schema 딕셔너리와 직접 작성한 검증 함수도 각각 `schema`, `validator`에 전달할 수 있습니다.
|
|
536
537
|
- `example`은 문서용 값이며 자동 실행·검증하지 않습니다.
|
|
537
538
|
- `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
|
|
539
|
+
- Run의 `name`은 AsyncAPI Operation 키와 Metrics 작업 키에 함께 사용합니다.
|
|
540
|
+
생략하면 `모듈명.__qualname__`을 사용하며, Run의 `description`은 AsyncAPI Operation에 표시됩니다.
|
|
538
541
|
- 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
|
|
539
542
|
|
|
540
543
|
검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Run 에 이름(name)과 설명(description) 지원 구현 기록
|
|
2
|
+
|
|
3
|
+
## 1. 구현 개요
|
|
4
|
+
|
|
5
|
+
[요구사항](./REQUIREMENTS.md)에 따라 Run 클래스 정의에 `name`과 `description`을
|
|
6
|
+
추가하고, 이름을 AsyncAPI와 Metrics의 작업 식별자로 연결했다. 이름 미지정 시
|
|
7
|
+
`모듈명.__qualname__`을 사용한다. 설명은 AsyncAPI Operation에만 반영한다.
|
|
8
|
+
|
|
9
|
+
- 구현·검증일: 2026-10-20
|
|
10
|
+
- 사용자용 안내: [변경 로그](./README.md)
|
|
11
|
+
|
|
12
|
+
## 2. 구현 내용
|
|
13
|
+
|
|
14
|
+
- [Run](../../../src/Worker/Run.py) 클래스 정의에서 `name`과 `description`
|
|
15
|
+
키워드를 받는다. `RunMeta`가 이름을 내부 키워드 `run_name`으로 중계하고,
|
|
16
|
+
`__init_subclass__`가 이름과 설명을 처리한다. 이름은 Python 식별자이며
|
|
17
|
+
예약어가 아닌지 검사하고, 설명은 문자열 또는 `None`으로 검증한다.
|
|
18
|
+
- 클래스 생성 시 이름이 생략되면 해당 클래스의 `__module__`과 `__qualname__`으로
|
|
19
|
+
기본 이름을 만든다. 하위 Run에는 상위 Run의 이름과 설명을 상속하지 않는다.
|
|
20
|
+
- [Worker](../../../src/Worker/Worker.py)는 Run 등록 시 동일한 이름이 이미
|
|
21
|
+
등록되어 있는지 검사하고 중복을 `ValueError`로 거부한다. 메시지를 Metrics에
|
|
22
|
+
전달하는 식별자도 `run.name`을 사용한다.
|
|
23
|
+
- [Specifications](../../../src/Inspector/Extensions/Specifications.py)은
|
|
24
|
+
`operations` 키에 `task.name`을 사용하고, 지정된 `task.description`을 해당
|
|
25
|
+
Operation에 기록한다. 문서 생성 시에도 중복 Operation 키를 거부한다.
|
|
26
|
+
- [Metrics](../../../src/Inspector/Extensions/Metrics.py)는 초기 작업 통계 맵의
|
|
27
|
+
키를 `task.name`으로 만들며, 중복 키를 발견하면 통계 맵을 부분 초기화하지 않고
|
|
28
|
+
`ValueError`를 발생시킨다.
|
|
29
|
+
- [Specification](../../../src/Worker/Specification.py)에서 `description` 인자와
|
|
30
|
+
AsyncAPI 메시지 설명 생성을 제거했다. schema, validator, example, format은
|
|
31
|
+
유지한다.
|
|
32
|
+
- [RabbitMQ Worker 샘플](../../../sample/Worker/RabbitMQ/Tasks.py)과
|
|
33
|
+
[Redis Worker 샘플](../../../sample/Worker/Redis/Sample.py)에 `add`, `divide`
|
|
34
|
+
이름 및 작업 설명을 지정했다. RabbitMQ 샘플의 request·response
|
|
35
|
+
Specification 설명은 Run의 메타데이터로 옮겼다.
|
|
36
|
+
- [한국어 사용 설명서](../../../README.md)와
|
|
37
|
+
[영어 사용 설명서](../../../README.en.md)에 새 선언 방법을 반영했다.
|
|
38
|
+
|
|
39
|
+
## 3. 사용 방법
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from DefStream.Worker import Run, Specification
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Add(
|
|
46
|
+
Run,
|
|
47
|
+
name='add',
|
|
48
|
+
description='Add two integers',
|
|
49
|
+
request=Specification(schema=request_schema),
|
|
50
|
+
response=Specification(schema=response_schema),
|
|
51
|
+
):
|
|
52
|
+
def run(
|
|
53
|
+
self,
|
|
54
|
+
a: int,
|
|
55
|
+
b: int,
|
|
56
|
+
):
|
|
57
|
+
return a + b
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
위 Run은 AsyncAPI `operations`와 Metrics에서 모두 `add`라는 키를 사용한다.
|
|
61
|
+
`name`을 생략하면 Run 모듈명과 클래스의 `__qualname__`을 결합한 이름을 사용한다.
|
|
62
|
+
`description`은 AsyncAPI Operation에만 나타나며 request·response 메시지 설명에는
|
|
63
|
+
적용하지 않는다.
|
|
64
|
+
|
|
65
|
+
## 4. 호환성 영향
|
|
66
|
+
|
|
67
|
+
- `Specification(description=...)`는 더 이상 지원하지 않는다. 작업 설명은 Run
|
|
68
|
+
클래스 정의의 `description`으로 옮겨야 한다.
|
|
69
|
+
- AsyncAPI Operation 이름을 지정하지 않으면 기존 클래스명 단독 키 대신
|
|
70
|
+
`모듈명.__qualname__`이 사용된다. 이 키를 참조하는 클라이언트는 갱신해야 한다.
|
|
71
|
+
- Metrics 작업 키는 기본값에서 기존 `모듈명.__qualname__` 형식을 유지한다.
|
|
72
|
+
- 동일한 유효 이름의 Run은 Worker 등록에서 거부한다. Metrics 및 AsyncAPI 문서
|
|
73
|
+
생성 경계에서도 덮어쓰기를 방지하도록 중복을 검사한다.
|
|
74
|
+
|
|
75
|
+
## 5. 검증 결과
|
|
76
|
+
|
|
77
|
+
### 자동 테스트 및 정적 진단
|
|
78
|
+
|
|
79
|
+
- 최초 Python 3.12 검증의 `unittest discover -s test`: **199개 실행 중
|
|
80
|
+
191개 통과, 8개 건너뜀**. 이 결과는 Python 3.9·3.10 호환성 검증을 포함하지 않았다.
|
|
81
|
+
- Run 이름·설명, 입력 검증, 기본 이름, 상속 비적용, 등록·문서 충돌,
|
|
82
|
+
AsyncAPI 설명과 Metrics 키의 회귀 테스트를 추가했다.
|
|
83
|
+
- 변경한 `Run.py`, `Specification.py`, `Metrics.py`, `Specifications.py`에
|
|
84
|
+
Pylance 진단 오류가 없다.
|
|
85
|
+
- `Run.__init_subclass__`의 기존 호출 지점과 새 시그니처가 호환됨을 확인했다.
|
|
86
|
+
- RabbitMQ와 Redis 샘플 파일의 `py_compile` 검사를 통과했다.
|
|
87
|
+
|
|
88
|
+
### Docker 브로커 실행
|
|
89
|
+
|
|
90
|
+
로컬 Docker에 임시 RabbitMQ `3.13-management`와 Redis `7.4-alpine`을 실행하고
|
|
91
|
+
각 Worker 샘플을 실제 브로커에 연결했다. Redis는 샘플 기본 연결 설정을 사용했고,
|
|
92
|
+
RabbitMQ는 로컬 guest 계정을 사용했다.
|
|
93
|
+
|
|
94
|
+
| 샘플 | AsyncAPI·Health | Metrics 처리 결과 | 브로커 상태 |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| RabbitMQ | `/status` 200, Operation 키 `add`, `divide` 및 설명 확인 | 격리 확인에서 각 작업 5건 성공, active 0 | 두 큐의 ready·unacknowledged 모두 0 |
|
|
97
|
+
| Redis | `/status` 200, Operation 키 `add`, `divide` 및 설명 확인 | Add 5건 성공, Divide 4건 성공·1건 실패, active 0 | 각 스트림에 샘플 메시지 5건 기록 |
|
|
98
|
+
|
|
99
|
+
Redis의 Divide 실패는 샘플이 의도적으로 넣는 0 나눗셈 입력에서 발생했다.
|
|
100
|
+
각 샘플이 모두 Inspector 기본 포트 `9999`를 사용하므로 HTTP 응답은 워커를
|
|
101
|
+
하나씩 분리하여 확인했다. 검증 후 Worker 프로세스와 임시 컨테이너를 종료하고
|
|
102
|
+
샘플 프로세스 및 포트 리스너가 남지 않았음을 확인했다.
|
|
103
|
+
|
|
104
|
+
전체 테스트 중 8개는 선택적 통합 테스트로 건너뛰었다. 이번 로컬 Docker 실행은
|
|
105
|
+
브로커와 샘플의 실제 연동 검증이며, 전체 브로커 어댑터 테스트 스위트 실행으로
|
|
106
|
+
간주하지 않는다.
|
|
107
|
+
|
|
108
|
+
## 6. Python 3.9·3.10 호환성 수정
|
|
109
|
+
|
|
110
|
+
Python 3.9.13과 3.10.11에서 클래스 정의의 `name=` 키워드가
|
|
111
|
+
`ABCMeta.__new__(mcls, name, bases, namespace, **kwargs)`의 클래스 이름 인자와
|
|
112
|
+
충돌하는 것을 재현했다. 이 경우 `Run.__init_subclass__`에 도달하기 전에
|
|
113
|
+
`TypeError: __new__() got multiple values for argument 'name'`이 발생한다.
|
|
114
|
+
|
|
115
|
+
`ABCMeta`를 상속한 `RunMeta`를 추가하여 공개 `name=`을 내부 `run_name` 키워드로
|
|
116
|
+
변환한 뒤 기존 추상 클래스 생성 경로로 전달한다. 이름의 기본값·검증·상속 비적용
|
|
117
|
+
처리는 계속 `Run.__init_subclass__`가 담당한다. 사용자 클래스 정의 문법은
|
|
118
|
+
변경하지 않으며 `ABCMeta`의 추상 메서드 검사도 유지한다.
|
|
119
|
+
|
|
120
|
+
[Worker 테스트](../../../test/test_worker.py)에 이름을 지정한 추상 Run의 생성 거부,
|
|
121
|
+
구체 하위 Run의 이름 재정의 및 실행을 검증하는 회귀 테스트를 추가했다.
|
|
122
|
+
|
|
123
|
+
| Python | 전체 코어 테스트 결과 |
|
|
124
|
+
|---|---|
|
|
125
|
+
| 3.9.13 | 200개 실행, 192개 통과, 8개 건너뜀 |
|
|
126
|
+
| 3.10.11 | 200개 실행, 192개 통과, 8개 건너뜀 |
|
|
127
|
+
| 3.12.10 | 200개 실행, 192개 통과, 8개 건너뜀 |
|
|
128
|
+
| 3.14.5 | 200개 실행, 192개 통과, 8개 건너뜀 |
|
|
129
|
+
|
|
130
|
+
구버전 테스트는 개발 환경의 Redis 8.x 클라이언트가 해당 버전과 호환되지 않아
|
|
131
|
+
세션 전용 경로에 Redis 5.3.1과 의존성을 설치하여 실행했다. 프로젝트의
|
|
132
|
+
의존성 선언과 공용 가상환경은 변경하지 않았다.
|
|
133
|
+
Python 3.9의 첫 전체 실행에서는 무관한 Console HTTP 테스트에 Windows
|
|
134
|
+
`ConnectionAbortedError`가 발생했으나, 해당 테스트의 단독 실행과 전체 재실행은
|
|
135
|
+
통과했다. 변경한 Run 소스의 Pylance 진단에도 오류가 없다.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Run 에 이름(name)과 설명(description) 지원
|
|
2
|
+
|
|
3
|
+
## 변경 요약
|
|
4
|
+
|
|
5
|
+
Run 정의에서 `name`과 `description`을 지정할 수 있습니다. 지정한 이름은 AsyncAPI
|
|
6
|
+
`operations`와 Metrics의 작업 키에서 함께 사용됩니다. `name`을 생략하면
|
|
7
|
+
`모듈명.__qualname__`이 기본 이름입니다.
|
|
8
|
+
|
|
9
|
+
## 사용 방법
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from DefStream.Worker import Run, Specification
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Add(
|
|
16
|
+
Run,
|
|
17
|
+
name='add',
|
|
18
|
+
description='Add two integers',
|
|
19
|
+
request=Specification(schema=request_schema),
|
|
20
|
+
response=Specification(schema=response_schema),
|
|
21
|
+
):
|
|
22
|
+
def run(
|
|
23
|
+
self,
|
|
24
|
+
a: int,
|
|
25
|
+
b: int,
|
|
26
|
+
):
|
|
27
|
+
return a + b
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run 등록 후 AsyncAPI Operation 키와 Metrics 작업 식별자는 `add`입니다.
|
|
31
|
+
이름은 Python 식별자여야 하며, Worker에 같은 이름을 가진 Run을 함께 등록할 수
|
|
32
|
+
없습니다.
|
|
33
|
+
|
|
34
|
+
## 기존 코드에 미치는 영향
|
|
35
|
+
|
|
36
|
+
- `Specification(description=...)`는 제거되었습니다. 설명을 Run의
|
|
37
|
+
`description`으로 옮기세요. 설명은 AsyncAPI Operation에 반영되며 메시지 설명에는
|
|
38
|
+
적용하지 않습니다.
|
|
39
|
+
- AsyncAPI Operation의 기본 키는 클래스명에서 `모듈명.__qualname__`으로
|
|
40
|
+
변경됩니다. Operation 키를 사용하는 클라이언트 설정도 갱신하세요.
|
|
41
|
+
- Metrics의 기본 작업 키는 기존 qualified 클래스 이름 형식과 같습니다.
|
|
42
|
+
- Run 하위 클래스는 상위 Run의 `name`이나 `description`을 상속하지 않습니다.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Run 에 이름(name)과 설명(description) 지원
|
|
2
|
+
|
|
3
|
+
## 1. 목적
|
|
4
|
+
|
|
5
|
+
Run을 정의할 때 이름과 설명을 선택적으로 지정할 수 있도록 한다. 지정한
|
|
6
|
+
이름을 AsyncAPI `operations`의 키와 Metrics의 작업 키로 공통 사용하며,
|
|
7
|
+
이름을 지정하지 않으면 `모듈.클래스명`을 사용한다. 설명은 AsyncAPI Operation에
|
|
8
|
+
반영한다. `Specification`에서는 `description`을 제거한다.
|
|
9
|
+
|
|
10
|
+
본 문서는 구현 전 요구사항 명세이다.
|
|
11
|
+
|
|
12
|
+
## 2. 범위
|
|
13
|
+
|
|
14
|
+
### 포함
|
|
15
|
+
|
|
16
|
+
- Run 정의 시 선택적 `name` 및 `description` 지정.
|
|
17
|
+
- Run별 유효 이름 결정 및 AsyncAPI와 Metrics에서의 일관된 사용.
|
|
18
|
+
- 이름의 입력 검증, 충돌 처리, 기본 이름과 기존 동작의 호환성 검증.
|
|
19
|
+
- `Specification`에서 `description` 제거 및 설명의 Run 단위 관리.
|
|
20
|
+
|
|
21
|
+
### 제외
|
|
22
|
+
|
|
23
|
+
- AsyncAPI 채널 주소, 메시지 키·이름 또는 payload 변경.
|
|
24
|
+
- Metrics 집계 방식, 지표 이름, 기간 및 응답 형식 변경.
|
|
25
|
+
- Run의 실행·검증·메시지 처리 방식 변경.
|
|
26
|
+
|
|
27
|
+
## 3. 용어 및 현재 동작
|
|
28
|
+
|
|
29
|
+
| 용어 | 의미 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Run 이름 | Run 정의에 선택적으로 지정하는 사용자 제공 식별자 |
|
|
32
|
+
| 유효 Run 이름 | AsyncAPI와 Metrics에서 해당 Run을 식별하는 공통 문자열 |
|
|
33
|
+
| 기본 이름 | 명시적 이름이 없을 때 사용하는 `Run.__module__ + '.' + Run.__qualname__` |
|
|
34
|
+
| Run 설명 | Run 정의에 선택적으로 지정하여 AsyncAPI Operation에 반영하는 문자열 |
|
|
35
|
+
|
|
36
|
+
- AsyncAPI `operations`의 현재 키는 Run 클래스의 `__name__`이다.
|
|
37
|
+
- Metrics의 현재 작업 키는 Run 클래스의 `__module__`과 `__qualname__`을
|
|
38
|
+
결합한 값이다.
|
|
39
|
+
- 변경 후에는 AsyncAPI와 Metrics가 모두 동일한 유효 Run 이름을 사용한다.
|
|
40
|
+
- 현재 `Specification`은 `description`을 받아 메시지 및 Operation 설명에
|
|
41
|
+
활용한다. 변경 후 설명은 Run 정의에서 관리하고 Specification에는 두지 않는다.
|
|
42
|
+
|
|
43
|
+
## 4. 기능 요구사항
|
|
44
|
+
|
|
45
|
+
| ID | 요구사항 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| SPECNAME-01 | Run 정의 시 선택적 `name`을 지정할 수 있어야 한다. 지정하지 않은 경우의 기본값은 미지정 상태여야 한다. |
|
|
48
|
+
| SPECNAME-02 | Run 정의 시 선택적 `description`을 지정할 수 있어야 한다. 지정하지 않은 경우 설명은 없어야 한다. |
|
|
49
|
+
| SPECNAME-03 | `name`은 Python 문법상 유효한 식별자여야 하며 예약어는 허용하지 않는다. 이름을 자동 정규화하거나 변경하지 않는다. 잘못된 타입은 `TypeError`, 유효하지 않은 식별자나 예약어는 `ValueError`로 거부한다. |
|
|
50
|
+
| SPECNAME-04 | `description`은 문자열 또는 `None`이어야 한다. 다른 타입은 `TypeError`로 명확히 거부한다. 빈 설명 문자열은 그대로 보존한다. |
|
|
51
|
+
| SPECNAME-05 | 이름이 지정되지 않은 Run의 유효 이름은 `Run.__module__ + '.' + Run.__qualname__`이어야 한다. 중첩 클래스도 `__qualname__`을 포함해야 한다. |
|
|
52
|
+
| SPECNAME-06 | AsyncAPI 문서의 `operations` 객체에서 각 Run의 키로 유효 Run 이름을 사용해야 한다. 이 요구사항은 Operation 객체의 별도 필드가 아니라 `operations` 맵의 키에 적용한다. |
|
|
53
|
+
| SPECNAME-07 | Run의 `description`이 지정된 경우 AsyncAPI Operation의 `description`으로 사용해야 한다. 지정하지 않은 경우 해당 필드는 생성하지 않는다. |
|
|
54
|
+
| SPECNAME-08 | `Specification` 생성자는 `description` 인자를 더 이상 받지 않아야 한다. 기존 description positional 인자에 의존하는 호출은 Run의 `description`으로 이전해야 하며, 제거된 인자를 전달하면 기존 Python 호출 규칙에 따라 명확히 실패해야 한다. |
|
|
55
|
+
| SPECNAME-09 | Metrics의 작업 저장 키와 해당 작업을 식별하는 출력 키·레이블은 유효 Run 이름을 사용해야 한다. Metrics 집계 내용과 지표 구조는 변경하지 않는다. |
|
|
56
|
+
| SPECNAME-10 | 서로 다른 Run이 같은 유효 이름을 가지면 Run 등록 시와 AsyncAPI 문서 생성 시 충돌을 감지해야 한다. 데이터가 조용히 덮어써지거나 합쳐지지 않도록 원인을 알 수 있는 예외로 거부한다. |
|
|
57
|
+
| SPECNAME-11 | Run에 이름을 지정하지 않은 경우 AsyncAPI와 Metrics 모두 같은 `모듈.클래스명` 기본 이름을 사용해야 한다. 이는 기존 AsyncAPI의 클래스명 단독 키를 qualified 이름으로 바꾸는 의도된 호환성 변경이다. |
|
|
58
|
+
| SPECNAME-12 | Run의 `name`과 `description`은 해당 Run 정의에 적용하며, 하위 Run 정의가 값을 지정하지 않으면 상위 Run의 메타데이터를 상속하지 않는다. 이름 미지정 하위 Run은 자체 모듈·클래스명으로 식별되고 설명도 미지정 상태가 된다. |
|
|
59
|
+
|
|
60
|
+
## 5. 동작 보장 및 호환성
|
|
61
|
+
|
|
62
|
+
- 사용자 지정 이름은 AsyncAPI와 Metrics에서 동일한 문자열로 사용한다.
|
|
63
|
+
- 이름은 AsyncAPI Operation의 키와 Metrics 작업 식별자에만 적용한다.
|
|
64
|
+
채널 주소, request/response 메시지 이름과 payload 구성은 기존 규칙을 유지한다.
|
|
65
|
+
- 설명은 AsyncAPI Operation에만 적용한다. request/response 메시지 설명은
|
|
66
|
+
Specification에서 제거되며, 이번 변경에서 별도 대체 메타데이터를 추가하지 않는다.
|
|
67
|
+
- 이름이 없는 Run의 Metrics 키 형식은 기존과 같다. AsyncAPI 키는 기존의
|
|
68
|
+
클래스명 단독 형식에서 `모듈.클래스명` 형식으로 변경된다.
|
|
69
|
+
- 이름 및 설명 지정 여부와 관계없이 Specification의 schema, validator,
|
|
70
|
+
example, format 검증 및 payload 문서화 동작을 유지한다. 단,
|
|
71
|
+
Specification의 `description`은 의도적으로 제거한다.
|
|
72
|
+
- 중복 이름을 허용하지 않는 기존 AsyncAPI 동작을 보존하고, Metrics에서도
|
|
73
|
+
이름 중복으로 작업 데이터가 유실되지 않도록 한다.
|
|
74
|
+
|
|
75
|
+
## 6. 인수 기준
|
|
76
|
+
|
|
77
|
+
| ID | 검증 시나리오 및 기대 결과 |
|
|
78
|
+
|---|---|
|
|
79
|
+
| WAT-01 | `Run` 정의에 `name='orders'`를 지정하면 AsyncAPI `operations`와 Metrics에서 `orders` 키로 식별된다. |
|
|
80
|
+
| WAT-02 | 이름을 지정하지 않은 모듈 최상위 Run의 AsyncAPI 키와 Metrics 키가 모두 `모듈명.클래스명`이다. |
|
|
81
|
+
| WAT-03 | 이름을 지정하지 않은 중첩 Run의 기본 이름에 모듈명과 중첩 경로를 포함한 `__qualname__`이 반영된다. |
|
|
82
|
+
| WAT-04 | `name`이 Python 문법상 식별자가 아니거나 예약어이거나 문자열이 아니면 각각 `ValueError` 또는 `TypeError`가 발생한다. |
|
|
83
|
+
| WAT-05 | `description`이 문자열이면 AsyncAPI Operation 설명에 반영되고, `None`이면 Operation에 설명 필드가 없다. |
|
|
84
|
+
| WAT-06 | `description`에 문자열 또는 `None` 이외의 값을 전달하면 `TypeError`가 발생한다. |
|
|
85
|
+
| WAT-07 | 두 Run에 같은 사용자 지정 이름을 지정하면 Run 등록 시 충돌을 감지하고, 문서 생성에서도 충돌을 검증한다. 기존 항목을 덮어쓰거나 통계를 합치지 않는다. |
|
|
86
|
+
| WAT-08 | Run의 하위 클래스가 이름과 설명을 재지정하지 않으면 상위 값이 상속되지 않는다. 하위 Run은 자신의 기본 이름을 사용하고 설명은 미지정으로 남는다. |
|
|
87
|
+
| WAT-09 | `Specification(description=...)` 호출은 지원되지 않으며, 이를 사용하는 기존 코드는 Run의 설명 지정 방식으로 이전해야 한다. |
|
|
88
|
+
| WAT-10 | 채널 주소, request/response 메시지 키, payload, Metrics 집계값이 이름 지정 전후 기존 계약을 유지한다. |
|
|
@@ -65,8 +65,9 @@ class Add(
|
|
|
65
65
|
'queue': 'add',
|
|
66
66
|
},
|
|
67
67
|
on_error=Requeue(),
|
|
68
|
+
name='add',
|
|
69
|
+
description='Add two integers',
|
|
68
70
|
request=Specification(
|
|
69
|
-
description='Addition request',
|
|
70
71
|
schema=Object(
|
|
71
72
|
properties=Properties(
|
|
72
73
|
a=Integer('First operand'),
|
|
@@ -89,7 +90,6 @@ class Add(
|
|
|
89
90
|
},
|
|
90
91
|
),
|
|
91
92
|
response=Specification(
|
|
92
|
-
description='Addition response',
|
|
93
93
|
schema=Integer('Sum'),
|
|
94
94
|
validator=Validator(IsInteger()),
|
|
95
95
|
example=30,
|
|
@@ -115,8 +115,9 @@ class Divide(
|
|
|
115
115
|
queue='divide',
|
|
116
116
|
),
|
|
117
117
|
on_error=Reject(),
|
|
118
|
+
name='divide',
|
|
119
|
+
description='Divide a dividend by a divisor',
|
|
118
120
|
request=Specification(
|
|
119
|
-
description='Division request',
|
|
120
121
|
schema=Object(
|
|
121
122
|
properties=Properties(
|
|
122
123
|
a=Integer('Dividend'),
|
|
@@ -139,7 +140,6 @@ class Divide(
|
|
|
139
140
|
},
|
|
140
141
|
),
|
|
141
142
|
response=Specification(
|
|
142
|
-
description='Division response',
|
|
143
143
|
schema=Number('Quotient'),
|
|
144
144
|
validator=Validator(IsNumber()),
|
|
145
145
|
example=5.0,
|
|
@@ -56,6 +56,8 @@ def configure_logging() -> None:
|
|
|
56
56
|
class Add(
|
|
57
57
|
Run,
|
|
58
58
|
parameters=Subscription('add'),
|
|
59
|
+
name='add',
|
|
60
|
+
description='Add two integers',
|
|
59
61
|
):
|
|
60
62
|
def run(
|
|
61
63
|
self,
|
|
@@ -76,6 +78,8 @@ class Divide(
|
|
|
76
78
|
Run,
|
|
77
79
|
parameters=Subscription('divide'),
|
|
78
80
|
on_error=Reject(),
|
|
81
|
+
name='divide',
|
|
82
|
+
description='Divide a dividend by a divisor',
|
|
79
83
|
):
|
|
80
84
|
def run(
|
|
81
85
|
self,
|
|
@@ -122,13 +122,15 @@ class Metrics(Extension):
|
|
|
122
122
|
worker = inspector.worker
|
|
123
123
|
if worker is None:
|
|
124
124
|
raise RuntimeError('Metrics requires an attached Worker')
|
|
125
|
+
tasks = {}
|
|
126
|
+
for task in worker.tasks:
|
|
127
|
+
if task.name in tasks:
|
|
128
|
+
raise ValueError('Duplicate Metrics task name: ' + task.name)
|
|
129
|
+
tasks[task.name] = TaskMetrics()
|
|
125
130
|
with self.lock:
|
|
126
131
|
self.started_at = monotonic()
|
|
127
132
|
self.instance = uuid4().hex
|
|
128
|
-
self.tasks =
|
|
129
|
-
task.__module__ + '.' + task.__qualname__: TaskMetrics()
|
|
130
|
-
for task in worker.tasks
|
|
131
|
-
}
|
|
133
|
+
self.tasks = tasks
|
|
132
134
|
self.logger.info(
|
|
133
135
|
'Metrics collection initialized: path=%s tasks=%d',
|
|
134
136
|
self.path,
|
|
@@ -3,6 +3,7 @@ from json import dumps
|
|
|
3
3
|
from logging import getLogger
|
|
4
4
|
from re import fullmatch
|
|
5
5
|
from typing import (
|
|
6
|
+
Any,
|
|
6
7
|
TYPE_CHECKING,
|
|
7
8
|
Optional,
|
|
8
9
|
)
|
|
@@ -72,10 +73,10 @@ class Specifications(Extension):
|
|
|
72
73
|
}
|
|
73
74
|
if self.description is not None:
|
|
74
75
|
info['description'] = self.description
|
|
75
|
-
channels = {}
|
|
76
|
-
operations = {}
|
|
76
|
+
channels: dict[str, Any] = {}
|
|
77
|
+
operations: dict[str, Any] = {}
|
|
77
78
|
for task in worker.tasks:
|
|
78
|
-
key = task.
|
|
79
|
+
key = task.name
|
|
79
80
|
if key in operations:
|
|
80
81
|
raise ValueError('Duplicate task specification identifier: ' + key)
|
|
81
82
|
parameters = task.parameters
|
|
@@ -102,7 +103,7 @@ class Specifications(Extension):
|
|
|
102
103
|
raise ValueError('Task specification requires a nonempty queue address: ' + key)
|
|
103
104
|
if queue in channels:
|
|
104
105
|
raise ValueError('Duplicate channel identifier: ' + queue)
|
|
105
|
-
message = task.request.document() if task.request is not None else {
|
|
106
|
+
message: dict[str, Any] = task.request.document() if task.request is not None else {
|
|
106
107
|
'payload': {'type': 'object'},
|
|
107
108
|
}
|
|
108
109
|
message['name'] = task.__name__ + 'Request'
|
|
@@ -123,8 +124,8 @@ class Specifications(Extension):
|
|
|
123
124
|
'request',
|
|
124
125
|
)],
|
|
125
126
|
}
|
|
126
|
-
if task.
|
|
127
|
-
operation['description'] = task.
|
|
127
|
+
if task.description is not None:
|
|
128
|
+
operation['description'] = task.description
|
|
128
129
|
if task.response is not None:
|
|
129
130
|
channels[queue]['messages']['response'] = task.response.document()
|
|
130
131
|
operation['reply'] = {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from abc import (
|
|
2
2
|
ABC,
|
|
3
|
+
ABCMeta,
|
|
3
4
|
abstractmethod,
|
|
4
5
|
)
|
|
5
6
|
from collections.abc import Mapping
|
|
7
|
+
from keyword import iskeyword
|
|
6
8
|
from logging import getLogger
|
|
7
9
|
from time import monotonic
|
|
8
10
|
from typing import (
|
|
9
11
|
Any,
|
|
12
|
+
Dict,
|
|
10
13
|
Optional,
|
|
11
14
|
Sequence,
|
|
12
15
|
Tuple,
|
|
@@ -81,9 +84,33 @@ class RunError(ABC):
|
|
|
81
84
|
raise NotImplementedError
|
|
82
85
|
|
|
83
86
|
|
|
84
|
-
class
|
|
87
|
+
class RunMeta(ABCMeta):
|
|
88
|
+
"""구버전 ABCMeta의 클래스 이름 인자와 Run의 name 키워드 충돌을 방지한다."""
|
|
89
|
+
|
|
90
|
+
def __new__(
|
|
91
|
+
mcls,
|
|
92
|
+
class_name: str,
|
|
93
|
+
bases: Tuple[type, ...],
|
|
94
|
+
namespace: Dict[str, Any],
|
|
95
|
+
/,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> 'RunMeta':
|
|
98
|
+
if 'name' in kwargs:
|
|
99
|
+
kwargs['run_name'] = kwargs.pop('name')
|
|
100
|
+
return super().__new__(
|
|
101
|
+
mcls,
|
|
102
|
+
class_name,
|
|
103
|
+
bases,
|
|
104
|
+
namespace,
|
|
105
|
+
**kwargs,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Run(ABC, metaclass=RunMeta):
|
|
85
110
|
logger = getLogger('DefStream.Run')
|
|
86
111
|
parameters: Any = None
|
|
112
|
+
name: str = __module__ + '.' + __qualname__
|
|
113
|
+
description: Optional[str] = None
|
|
87
114
|
on_error: Optional[EventError] = None
|
|
88
115
|
setups: Tuple[RunSetup, ...] = ()
|
|
89
116
|
completes: Tuple[RunComplete, ...] = ()
|
|
@@ -94,6 +121,8 @@ class Run(ABC):
|
|
|
94
121
|
def __init_subclass__(
|
|
95
122
|
cls,
|
|
96
123
|
*,
|
|
124
|
+
run_name: Optional[str] = None,
|
|
125
|
+
description: Optional[str] = None,
|
|
97
126
|
parameters: Any = None,
|
|
98
127
|
on_error: Optional[EventError] = None,
|
|
99
128
|
setups: Optional[Union[RunSetup, Sequence[RunSetup]]] = None,
|
|
@@ -104,6 +133,20 @@ class Run(ABC):
|
|
|
104
133
|
**kwargs,
|
|
105
134
|
):
|
|
106
135
|
super().__init_subclass__(**kwargs)
|
|
136
|
+
if run_name is not None and not isinstance(
|
|
137
|
+
run_name,
|
|
138
|
+
str,
|
|
139
|
+
):
|
|
140
|
+
raise TypeError('Run name must be a string or None')
|
|
141
|
+
if run_name is not None and (not run_name.isidentifier() or iskeyword(run_name)):
|
|
142
|
+
raise ValueError('Run name must be a valid Python identifier')
|
|
143
|
+
if description is not None and not isinstance(
|
|
144
|
+
description,
|
|
145
|
+
str,
|
|
146
|
+
):
|
|
147
|
+
raise TypeError('Run description must be a string or None')
|
|
148
|
+
cls.name = run_name if run_name is not None else cls.__module__ + '.' + cls.__qualname__
|
|
149
|
+
cls.description = description
|
|
107
150
|
parents = tuple(base for base in cls.__mro__[1:] if issubclass(
|
|
108
151
|
base,
|
|
109
152
|
Run,
|
|
@@ -29,7 +29,6 @@ class Specification:
|
|
|
29
29
|
schema: Any = None,
|
|
30
30
|
validator: Optional[Callable[[Any], Any]] = None,
|
|
31
31
|
example: Any = ...,
|
|
32
|
-
description: Optional[str] = None,
|
|
33
32
|
format: str = 'application/json',
|
|
34
33
|
):
|
|
35
34
|
if not isinstance(
|
|
@@ -60,7 +59,6 @@ class Specification:
|
|
|
60
59
|
self.schema = schema
|
|
61
60
|
self.validator = validator
|
|
62
61
|
self.example = example
|
|
63
|
-
self.description = description
|
|
64
62
|
self.format = format
|
|
65
63
|
self.document()
|
|
66
64
|
|
|
@@ -84,8 +82,6 @@ class Specification:
|
|
|
84
82
|
'contentType': self.format,
|
|
85
83
|
'payload': payload,
|
|
86
84
|
}
|
|
87
|
-
if self.description is not None:
|
|
88
|
-
message['description'] = self.description
|
|
89
85
|
if self.example is not ...:
|
|
90
86
|
message['examples'] = [{'payload': deepcopy(self.example)}]
|
|
91
87
|
return message
|
|
@@ -102,6 +102,11 @@ class Worker:
|
|
|
102
102
|
task_parameters(run)
|
|
103
103
|
if run in self.tasks:
|
|
104
104
|
raise ValueError('Run is already registered')
|
|
105
|
+
if any(
|
|
106
|
+
registered.name == run.name
|
|
107
|
+
for registered in self.tasks
|
|
108
|
+
):
|
|
109
|
+
raise ValueError('Duplicate Run name: ' + run.name)
|
|
105
110
|
self.tasks.append(run)
|
|
106
111
|
self.logger.debug(
|
|
107
112
|
'Task registered: task=%s.%s',
|
|
@@ -246,7 +251,7 @@ class Worker:
|
|
|
246
251
|
self.pending -= 1
|
|
247
252
|
raise RuntimeError('Worker consumer is not initialized')
|
|
248
253
|
inspector = self.inspector
|
|
249
|
-
task = run.
|
|
254
|
+
task = run.name
|
|
250
255
|
self.logger.debug(
|
|
251
256
|
'Task message received: task=%s pending=%s',
|
|
252
257
|
task,
|