DefStream 0.1.9__tar.gz → 0.1.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {defstream-0.1.9 → defstream-0.1.11}/DefStream.egg-info/PKG-INFO +7 -2
  2. {defstream-0.1.9 → defstream-0.1.11}/DefStream.egg-info/SOURCES.txt +3 -0
  3. {defstream-0.1.9 → defstream-0.1.11}/PKG-INFO +7 -2
  4. {defstream-0.1.9 → defstream-0.1.11}/README.en.md +6 -1
  5. {defstream-0.1.9 → defstream-0.1.11}/README.md +6 -1
  6. defstream-0.1.11/issues/fix/10/IMPLEMENTS.md +150 -0
  7. defstream-0.1.11/issues/fix/10/README.md +44 -0
  8. defstream-0.1.11/issues/fix/10/REQUIREMENTS.md +88 -0
  9. {defstream-0.1.9 → defstream-0.1.11}/sample/Worker/RabbitMQ/Tasks.py +4 -4
  10. {defstream-0.1.9 → defstream-0.1.11}/sample/Worker/Redis/Sample.py +4 -0
  11. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/Metrics.py +6 -4
  12. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/Specifications.py +7 -6
  13. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Run.py +47 -1
  14. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Specification.py +0 -4
  15. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Worker.py +6 -1
  16. {defstream-0.1.9 → defstream-0.1.11}/test/test_dashboard.py +1 -1
  17. {defstream-0.1.9 → defstream-0.1.11}/test/test_inspector.py +71 -3
  18. {defstream-0.1.9 → defstream-0.1.11}/test/test_specifications.py +148 -19
  19. {defstream-0.1.9 → defstream-0.1.11}/test/test_worker.py +49 -0
  20. {defstream-0.1.9 → defstream-0.1.11}/.github/workflows/ci.yml +0 -0
  21. {defstream-0.1.9 → defstream-0.1.11}/.gitignore +0 -0
  22. {defstream-0.1.9 → defstream-0.1.11}/DefStream.egg-info/dependency_links.txt +0 -0
  23. {defstream-0.1.9 → defstream-0.1.11}/DefStream.egg-info/requires.txt +0 -0
  24. {defstream-0.1.9 → defstream-0.1.11}/DefStream.egg-info/top_level.txt +0 -0
  25. {defstream-0.1.9 → defstream-0.1.11}/LICENSE +0 -0
  26. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/1/IMPLEMENTS.md +0 -0
  27. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/1/README.md +0 -0
  28. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/1/REQUIREMENTS.md +0 -0
  29. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/1/TASK.md +0 -0
  30. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/1/ci.refs.json +0 -0
  31. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/8/IMPLEMENTS.md +0 -0
  32. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/8/README.md +0 -0
  33. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/8/REQUIREMENTS.md +0 -0
  34. {defstream-0.1.9 → defstream-0.1.11}/issues/feature/8/ci.refs.json +0 -0
  35. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/3/IMPLEMENTS.md +0 -0
  36. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/3/README.md +0 -0
  37. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/3/REQUIREMENTS.md +0 -0
  38. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/3/ci.refs.json +0 -0
  39. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/6/IMPLEMENTS.md +0 -0
  40. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/6/README.md +0 -0
  41. {defstream-0.1.9 → defstream-0.1.11}/issues/fix/6/REQUIREMENTS.md +0 -0
  42. {defstream-0.1.9 → defstream-0.1.11}/sample/Broker/Sample.py +0 -0
  43. {defstream-0.1.9 → defstream-0.1.11}/sample/Worker/RabbitMQ/Client.py +0 -0
  44. {defstream-0.1.9 → defstream-0.1.11}/sample/Worker/RabbitMQ/Worker.py +0 -0
  45. {defstream-0.1.9 → defstream-0.1.11}/sample/Worker/Sample.py +0 -0
  46. {defstream-0.1.9 → defstream-0.1.11}/setup.cfg +0 -0
  47. {defstream-0.1.9 → defstream-0.1.11}/setup.py +0 -0
  48. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Configuration.py +0 -0
  49. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Connection.py +0 -0
  50. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Consumer.py +0 -0
  51. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Errors.py +0 -0
  52. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Event.py +0 -0
  53. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Implements/__init__.py +0 -0
  54. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Policies.py +0 -0
  55. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Priority.py +0 -0
  56. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/Protocol.py +0 -0
  57. {defstream-0.1.9 → defstream-0.1.11}/src/Broker/__init__.py +0 -0
  58. {defstream-0.1.9 → defstream-0.1.11}/src/Descriptor/DataObject.py +0 -0
  59. {defstream-0.1.9 → defstream-0.1.11}/src/Descriptor/Types.py +0 -0
  60. {defstream-0.1.9 → defstream-0.1.11}/src/Descriptor/Value.py +0 -0
  61. {defstream-0.1.9 → defstream-0.1.11}/src/Descriptor/__init__.py +0 -0
  62. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extension.py +0 -0
  63. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/Console.html +0 -0
  64. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/Console.py +0 -0
  65. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/Dashboard.html +0 -0
  66. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/Dashboard.py +0 -0
  67. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/HealthCheck.py +0 -0
  68. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Extensions/__init__.py +0 -0
  69. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Inspector.py +0 -0
  70. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/Response.py +0 -0
  71. {defstream-0.1.9 → defstream-0.1.11}/src/Inspector/__init__.py +0 -0
  72. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Pattern.py +0 -0
  73. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Array.py +0 -0
  74. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Common.py +0 -0
  75. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Compare.py +0 -0
  76. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Condition.py +0 -0
  77. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/DataObject.py +0 -0
  78. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/DateTime.py +0 -0
  79. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Number.py +0 -0
  80. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Object.py +0 -0
  81. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Size.py +0 -0
  82. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/String.py +0 -0
  83. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/Type.py +0 -0
  84. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Patterns/__init__.py +0 -0
  85. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Validate.py +0 -0
  86. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/Validator.py +0 -0
  87. {defstream-0.1.9 → defstream-0.1.11}/src/Validator/__init__.py +0 -0
  88. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Executor.py +0 -0
  89. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Message.py +0 -0
  90. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Pool.py +0 -0
  91. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/Startup.py +0 -0
  92. {defstream-0.1.9 → defstream-0.1.11}/src/Worker/__init__.py +0 -0
  93. {defstream-0.1.9 → defstream-0.1.11}/src/__init__.py +0 -0
  94. {defstream-0.1.9 → defstream-0.1.11}/test/test_dashboard_ui.cjs +0 -0
  95. {defstream-0.1.9 → defstream-0.1.11}/test/test_descriptor.py +0 -0
  96. {defstream-0.1.9 → defstream-0.1.11}/test/test_inspector_console.py +0 -0
  97. {defstream-0.1.9 → defstream-0.1.11}/test/test_inspector_logging.py +0 -0
  98. {defstream-0.1.9 → defstream-0.1.11}/test/test_lifecycle_hooks.py +0 -0
  99. {defstream-0.1.9 → defstream-0.1.11}/test/test_logging.py +0 -0
  100. {defstream-0.1.9 → defstream-0.1.11}/test/test_pool_callbacks.py +0 -0
  101. {defstream-0.1.9 → defstream-0.1.11}/test/test_process_signals.py +0 -0
  102. {defstream-0.1.9 → defstream-0.1.11}/test/test_protocol_contracts.py +0 -0
  103. {defstream-0.1.9 → defstream-0.1.11}/test/test_release_workflow.py +0 -0
  104. {defstream-0.1.9 → defstream-0.1.11}/test/test_runner_hooks.py +0 -0
  105. {defstream-0.1.9 → defstream-0.1.11}/test/test_task_messages.py +0 -0
  106. {defstream-0.1.9 → defstream-0.1.11}/test/test_task_priorities.py +0 -0
  107. {defstream-0.1.9 → defstream-0.1.11}/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.9
3
+ Version: 0.1.11
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,10 @@ 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에 표시됩니다.
574
+ 명시적 이름은 `[A-Za-z0-9_.-]+` 형식이며 공백·한국어는 허용하지 않습니다.
575
+ 대소문자와 입력값을 그대로 보존합니다.
571
576
  - 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
572
577
 
573
578
  검증 예외는 작업 실패로 처리되어 오류 훅과 `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.9
3
+ Version: 0.1.11
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,10 @@ 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에 표시됩니다.
574
+ 명시적 이름은 `[A-Za-z0-9_.-]+` 형식이며 공백·한국어는 허용하지 않습니다.
575
+ 대소문자와 입력값을 그대로 보존합니다.
571
576
  - 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
572
577
 
573
578
  검증 예외는 작업 실패로 처리되어 오류 훅과 `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,10 @@ 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.
510
+ Explicit names must match `[A-Za-z0-9_.-]+`; whitespace and Korean characters are not allowed.
511
+ Names are case-sensitive and preserved exactly as provided.
507
512
  - Execution order: setup hooks → request validation → `run()` → response validation → completion hooks → reply/ACK.
508
513
 
509
514
  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,10 @@ 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에 표시됩니다.
541
+ 명시적 이름은 `[A-Za-z0-9_.-]+` 형식이며 공백·한국어는 허용하지 않습니다.
542
+ 대소문자와 입력값을 그대로 보존합니다.
538
543
  - 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
539
544
 
540
545
  검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
@@ -0,0 +1,150 @@
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__`가 이름과 설명을 처리한다. 명시적 이름은
17
+ `[A-Za-z0-9_.-]+` 전체 일치로 검사하고, 설명은 문자열 또는 `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 진단에도 오류가 없다.
136
+
137
+ ## 7. 명시적 이름의 허용 문자 변경
138
+
139
+ Python 식별자 제한을 제거하고 영문 대소문자·숫자·`_`·`.`·`-`를 하나 이상
140
+ 허용하도록 변경했다. 위치 제한은 없으며 Python 예약어도 허용한다.
141
+ 빈 문자열, 공백·개행, 한국어 등 다른 문자는 거부한다.
142
+ 대소문자 변환이나 공백 제거 없이 지정한 이름을 그대로 사용한다.
143
+ 미지정 이름의 `모듈명.__qualname__` 생성 규칙과 중복 검사는 유지한다.
144
+
145
+ 허용·거부 문자 조합과 입력값 보존, AsyncAPI Operation 키 및 Metrics
146
+ 스냅샷·Prometheus 레이블의 동일 이름 사용을 회귀 테스트로 검증한다.
147
+
148
+ - Python 3.12.10: 전체 201개 실행, 193개 통과, 선택적 테스트 8개 건너뜀.
149
+ - Python 3.9.13·3.10.11: 각 관련 테스트 43개 실행, 40개 통과, 3개 건너뜀.
150
+ - 변경한 Run 소스의 Pylance 진단 및 변경 파일 공백 검사 통과.
@@ -0,0 +1,44 @@
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
+ 명시적 이름은 영문 대소문자·숫자·`_`·`.`·`-`로만 구성된 한 글자 이상의 문자열입니다
32
+ (`Orders.create-v2_Job` 등). 위치 제한은 없고 Python 예약어도 허용합니다.
33
+ 공백·한국어·그 외 문자는 허용하지 않으며, 입력을 변환하지 않고 대소문자를
34
+ 구분합니다. Worker에 같은 이름을 가진 Run을 함께 등록할 수 없습니다.
35
+
36
+ ## 기존 코드에 미치는 영향
37
+
38
+ - `Specification(description=...)`는 제거되었습니다. 설명을 Run의
39
+ `description`으로 옮기세요. 설명은 AsyncAPI Operation에 반영되며 메시지 설명에는
40
+ 적용하지 않습니다.
41
+ - AsyncAPI Operation의 기본 키는 클래스명에서 `모듈명.__qualname__`으로
42
+ 변경됩니다. Operation 키를 사용하는 클라이언트 설정도 갱신하세요.
43
+ - Metrics의 기본 작업 키는 기존 qualified 클래스 이름 형식과 같습니다.
44
+ - 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`은 `[A-Za-z0-9_.-]+` 전체 일치 문자열이어야 한다. 영문 대소문자·숫자·`_`·`.`·`-`를 위치 제한 없이 허용하고 Python 예약어도 허용한다. 빈 문자열, 공백, 한국어 및 그 외 문자는 `ValueError`, 문자열과 `None` 이외의 타입은 `TypeError`로 거부한다. 입력을 정규화하지 않고 대소문자를 구분하여 보존한다. |
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 | `Orders.create-v2_Job`, 숫자만 있는 이름, Python 예약어 및 구분자로 시작·끝나는 이름을 그대로 허용한다. 빈 문자열, 한국어, 공백·개행 및 허용 범위 밖 문자는 `ValueError`, 문자열과 `None` 이외의 타입은 `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.__name__
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.request is not None and task.request.description is not None:
127
- operation['description'] = task.request.description
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
6
7
  from logging import getLogger
8
+ from re import fullmatch
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 Run(ABC):
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,23 @@ 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 fullmatch(
142
+ r'[A-Za-z0-9_.-]+',
143
+ run_name,
144
+ ) is None:
145
+ raise ValueError('Run name must contain only ASCII letters, digits, underscores, dots or hyphens')
146
+ if description is not None and not isinstance(
147
+ description,
148
+ str,
149
+ ):
150
+ raise TypeError('Run description must be a string or None')
151
+ cls.name = run_name if run_name is not None else cls.__module__ + '.' + cls.__qualname__
152
+ cls.description = description
107
153
  parents = tuple(base for base in cls.__mro__[1:] if issubclass(
108
154
  base,
109
155
  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.__module__ + '.' + run.__qualname__
254
+ task = run.name
250
255
  self.logger.debug(
251
256
  'Task message received: task=%s pending=%s',
252
257
  task,