DefStream 0.1.10__tar.gz → 0.1.12__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.10 → defstream-0.1.12}/DefStream.egg-info/PKG-INFO +3 -1
- {defstream-0.1.10 → defstream-0.1.12}/DefStream.egg-info/SOURCES.txt +1 -0
- {defstream-0.1.10 → defstream-0.1.12}/PKG-INFO +3 -1
- {defstream-0.1.10 → defstream-0.1.12}/README.en.md +2 -0
- {defstream-0.1.10 → defstream-0.1.12}/README.md +2 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/10/IMPLEMENTS.md +17 -2
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/10/README.md +4 -2
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/10/REQUIREMENTS.md +2 -2
- defstream-0.1.12/issues/fix/13/README.md +31 -0
- {defstream-0.1.10 → defstream-0.1.12}/sample/Worker/RabbitMQ/Client.py +2 -2
- {defstream-0.1.10 → defstream-0.1.12}/sample/Worker/RabbitMQ/Tasks.py +23 -1
- {defstream-0.1.10 → defstream-0.1.12}/sample/Worker/RabbitMQ/Worker.py +7 -4
- {defstream-0.1.10 → defstream-0.1.12}/sample/Worker/Redis/Sample.py +22 -15
- {defstream-0.1.10 → defstream-0.1.12}/sample/Worker/Sample.py +25 -16
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Run.py +6 -3
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Startup.py +42 -3
- {defstream-0.1.10 → defstream-0.1.12}/test/test_inspector.py +16 -2
- {defstream-0.1.10 → defstream-0.1.12}/test/test_logging.py +1 -1
- {defstream-0.1.10 → defstream-0.1.12}/test/test_specifications.py +35 -5
- {defstream-0.1.10 → defstream-0.1.12}/.github/workflows/ci.yml +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/.gitignore +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/DefStream.egg-info/dependency_links.txt +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/DefStream.egg-info/requires.txt +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/DefStream.egg-info/top_level.txt +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/LICENSE +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/1/IMPLEMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/1/README.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/1/REQUIREMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/1/TASK.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/1/ci.refs.json +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/8/IMPLEMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/8/README.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/8/REQUIREMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/feature/8/ci.refs.json +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/3/IMPLEMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/3/README.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/3/REQUIREMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/3/ci.refs.json +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/6/IMPLEMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/6/README.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/issues/fix/6/REQUIREMENTS.md +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/sample/Broker/Sample.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/setup.cfg +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/setup.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Configuration.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Connection.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Consumer.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Errors.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Event.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Implements/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Policies.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Priority.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/Protocol.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Broker/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Descriptor/DataObject.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Descriptor/Types.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Descriptor/Value.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Descriptor/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extension.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/Console.html +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/Console.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/Dashboard.html +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/Dashboard.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/HealthCheck.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/Metrics.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/Specifications.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Extensions/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Inspector.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/Response.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Inspector/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Pattern.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Array.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Common.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Compare.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Condition.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/DataObject.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/DateTime.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Number.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Object.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Size.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/String.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/Type.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Patterns/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Validate.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/Validator.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Validator/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Executor.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Message.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Pool.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Specification.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/Worker.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/Worker/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/src/__init__.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_dashboard.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_dashboard_ui.cjs +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_descriptor.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_inspector_console.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_inspector_logging.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_lifecycle_hooks.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_pool_callbacks.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_process_signals.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_protocol_contracts.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_release_workflow.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_runner_hooks.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_task_messages.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_task_priorities.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/test/test_worker.py +0 -0
- {defstream-0.1.10 → defstream-0.1.12}/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.12
|
|
4
4
|
Summary: Synchronous message processing with pluggable broker adapters
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -571,6 +571,8 @@ class Add(
|
|
|
571
571
|
- `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
|
|
572
572
|
- Run의 `name`은 AsyncAPI Operation 키와 Metrics 작업 키에 함께 사용합니다.
|
|
573
573
|
생략하면 `모듈명.__qualname__`을 사용하며, Run의 `description`은 AsyncAPI Operation에 표시됩니다.
|
|
574
|
+
명시적 이름은 `[A-Za-z0-9_.-]+` 형식이며 공백·한국어는 허용하지 않습니다.
|
|
575
|
+
대소문자와 입력값을 그대로 보존합니다.
|
|
574
576
|
- 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
|
|
575
577
|
|
|
576
578
|
검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DefStream
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: Synchronous message processing with pluggable broker adapters
|
|
5
5
|
Home-page: https://github.com/yong5eon/DefStream
|
|
6
6
|
Author: Heo Yongseon
|
|
@@ -571,6 +571,8 @@ class Add(
|
|
|
571
571
|
- `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
|
|
572
572
|
- Run의 `name`은 AsyncAPI Operation 키와 Metrics 작업 키에 함께 사용합니다.
|
|
573
573
|
생략하면 `모듈명.__qualname__`을 사용하며, Run의 `description`은 AsyncAPI Operation에 표시됩니다.
|
|
574
|
+
명시적 이름은 `[A-Za-z0-9_.-]+` 형식이며 공백·한국어는 허용하지 않습니다.
|
|
575
|
+
대소문자와 입력값을 그대로 보존합니다.
|
|
574
576
|
- 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
|
|
575
577
|
|
|
576
578
|
검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
|
|
@@ -507,6 +507,8 @@ class Add(
|
|
|
507
507
|
- `format` defaults to `application/json`. It changes documented media types, not broker codecs.
|
|
508
508
|
- Run's `name` is used as both the AsyncAPI Operation key and the Metrics task key.
|
|
509
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.
|
|
510
512
|
- Execution order: setup hooks → request validation → `run()` → response validation → completion hooks → reply/ACK.
|
|
511
513
|
|
|
512
514
|
Validation exceptions are task failures and follow error hooks and the `on_error`
|
|
@@ -538,6 +538,8 @@ class Add(
|
|
|
538
538
|
- `format`의 기본값은 `application/json`입니다. 문서의 미디어 타입만 바꾸며 브로커 코덱은 별도 설정입니다.
|
|
539
539
|
- Run의 `name`은 AsyncAPI Operation 키와 Metrics 작업 키에 함께 사용합니다.
|
|
540
540
|
생략하면 `모듈명.__qualname__`을 사용하며, Run의 `description`은 AsyncAPI Operation에 표시됩니다.
|
|
541
|
+
명시적 이름은 `[A-Za-z0-9_.-]+` 형식이며 공백·한국어는 허용하지 않습니다.
|
|
542
|
+
대소문자와 입력값을 그대로 보존합니다.
|
|
541
543
|
- 실행 순서는 초기화 훅 → 요청 검증 → `run()` → 응답 검증 → 완료 훅 → 응답 발행·ACK입니다.
|
|
542
544
|
|
|
543
545
|
검증 예외는 작업 실패로 처리되어 오류 훅과 `on_error` 정책을 따릅니다.
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
- [Run](../../../src/Worker/Run.py) 클래스 정의에서 `name`과 `description`
|
|
15
15
|
키워드를 받는다. `RunMeta`가 이름을 내부 키워드 `run_name`으로 중계하고,
|
|
16
|
-
`__init_subclass__`가 이름과 설명을 처리한다. 이름은
|
|
17
|
-
|
|
16
|
+
`__init_subclass__`가 이름과 설명을 처리한다. 명시적 이름은
|
|
17
|
+
`[A-Za-z0-9_.-]+` 전체 일치로 검사하고, 설명은 문자열 또는 `None`으로 검증한다.
|
|
18
18
|
- 클래스 생성 시 이름이 생략되면 해당 클래스의 `__module__`과 `__qualname__`으로
|
|
19
19
|
기본 이름을 만든다. 하위 Run에는 상위 Run의 이름과 설명을 상속하지 않는다.
|
|
20
20
|
- [Worker](../../../src/Worker/Worker.py)는 Run 등록 시 동일한 이름이 이미
|
|
@@ -133,3 +133,18 @@ Python 3.9.13과 3.10.11에서 클래스 정의의 `name=` 키워드가
|
|
|
133
133
|
Python 3.9의 첫 전체 실행에서는 무관한 Console HTTP 테스트에 Windows
|
|
134
134
|
`ConnectionAbortedError`가 발생했으나, 해당 테스트의 단독 실행과 전체 재실행은
|
|
135
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 진단 및 변경 파일 공백 검사 통과.
|
|
@@ -28,8 +28,10 @@ class Add(
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Run 등록 후 AsyncAPI Operation 키와 Metrics 작업 식별자는 `add`입니다.
|
|
31
|
-
이름은
|
|
32
|
-
|
|
31
|
+
명시적 이름은 영문 대소문자·숫자·`_`·`.`·`-`로만 구성된 한 글자 이상의 문자열입니다
|
|
32
|
+
(`Orders.create-v2_Job` 등). 위치 제한은 없고 Python 예약어도 허용합니다.
|
|
33
|
+
공백·한국어·그 외 문자는 허용하지 않으며, 입력을 변환하지 않고 대소문자를
|
|
34
|
+
구분합니다. Worker에 같은 이름을 가진 Run을 함께 등록할 수 없습니다.
|
|
33
35
|
|
|
34
36
|
## 기존 코드에 미치는 영향
|
|
35
37
|
|
|
@@ -46,7 +46,7 @@ Run을 정의할 때 이름과 설명을 선택적으로 지정할 수 있도록
|
|
|
46
46
|
|---|---|
|
|
47
47
|
| SPECNAME-01 | Run 정의 시 선택적 `name`을 지정할 수 있어야 한다. 지정하지 않은 경우의 기본값은 미지정 상태여야 한다. |
|
|
48
48
|
| SPECNAME-02 | Run 정의 시 선택적 `description`을 지정할 수 있어야 한다. 지정하지 않은 경우 설명은 없어야 한다. |
|
|
49
|
-
| SPECNAME-03 | `name`은
|
|
49
|
+
| SPECNAME-03 | 명시적 `name`은 `[A-Za-z0-9_.-]+` 전체 일치 문자열이어야 한다. 영문 대소문자·숫자·`_`·`.`·`-`를 위치 제한 없이 허용하고 Python 예약어도 허용한다. 빈 문자열, 공백, 한국어 및 그 외 문자는 `ValueError`, 문자열과 `None` 이외의 타입은 `TypeError`로 거부한다. 입력을 정규화하지 않고 대소문자를 구분하여 보존한다. |
|
|
50
50
|
| SPECNAME-04 | `description`은 문자열 또는 `None`이어야 한다. 다른 타입은 `TypeError`로 명확히 거부한다. 빈 설명 문자열은 그대로 보존한다. |
|
|
51
51
|
| SPECNAME-05 | 이름이 지정되지 않은 Run의 유효 이름은 `Run.__module__ + '.' + Run.__qualname__`이어야 한다. 중첩 클래스도 `__qualname__`을 포함해야 한다. |
|
|
52
52
|
| SPECNAME-06 | AsyncAPI 문서의 `operations` 객체에서 각 Run의 키로 유효 Run 이름을 사용해야 한다. 이 요구사항은 Operation 객체의 별도 필드가 아니라 `operations` 맵의 키에 적용한다. |
|
|
@@ -79,7 +79,7 @@ Run을 정의할 때 이름과 설명을 선택적으로 지정할 수 있도록
|
|
|
79
79
|
| WAT-01 | `Run` 정의에 `name='orders'`를 지정하면 AsyncAPI `operations`와 Metrics에서 `orders` 키로 식별된다. |
|
|
80
80
|
| WAT-02 | 이름을 지정하지 않은 모듈 최상위 Run의 AsyncAPI 키와 Metrics 키가 모두 `모듈명.클래스명`이다. |
|
|
81
81
|
| WAT-03 | 이름을 지정하지 않은 중첩 Run의 기본 이름에 모듈명과 중첩 경로를 포함한 `__qualname__`이 반영된다. |
|
|
82
|
-
| WAT-04 | `
|
|
82
|
+
| WAT-04 | `Orders.create-v2_Job`, 숫자만 있는 이름, Python 예약어 및 구분자로 시작·끝나는 이름을 그대로 허용한다. 빈 문자열, 한국어, 공백·개행 및 허용 범위 밖 문자는 `ValueError`, 문자열과 `None` 이외의 타입은 `TypeError`로 거부한다. |
|
|
83
83
|
| WAT-05 | `description`이 문자열이면 AsyncAPI Operation 설명에 반영되고, `None`이면 Operation에 설명 필드가 없다. |
|
|
84
84
|
| WAT-06 | `description`에 문자열 또는 `None` 이외의 값을 전달하면 `TypeError`가 발생한다. |
|
|
85
85
|
| WAT-07 | 두 Run에 같은 사용자 지정 이름을 지정하면 Run 등록 시 충돌을 감지하고, 문서 생성에서도 충돌을 검증한다. 기존 항목을 덮어쓰거나 통계를 합치지 않는다. |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Worker 시작 배너와 샘플 로깅 순서
|
|
2
|
+
|
|
3
|
+
## 변경 요약
|
|
4
|
+
|
|
5
|
+
Worker 시작 배너를 로깅 설정 여부와 관계없이 먼저 확인할 수 있도록 Worker
|
|
6
|
+
샘플의 로깅 설정 시점을 `WorkerSetup` 훅으로 옮겼습니다. 시작 배너는 터미널에서
|
|
7
|
+
청록·파랑 계열로 강조하며, ASCII 아트의 모양과 문구는 유지합니다.
|
|
8
|
+
|
|
9
|
+
## 변경 내용
|
|
10
|
+
|
|
11
|
+
- [Worker 샘플](../../../sample/Worker/Sample.py)은 `configure_logging()`을
|
|
12
|
+
실행 진입점에서 호출하는 대신 `SetupLogger` setup 훅에서 수행합니다.
|
|
13
|
+
Worker 생성·작업 등록 시점의 로그는 아직 설정되지 않은 핸들러에 의해 출력되지
|
|
14
|
+
않으며, 시작 배너가 샘플의 첫 화면 출력이 됩니다.
|
|
15
|
+
- [Startup](../../../src/Worker/Startup.py)은 stdout이 터미널이고 `NO_COLOR`
|
|
16
|
+
환경 변수가 설정되지 않은 경우에만 로고에 색을 적용합니다. `Starting Worker`
|
|
17
|
+
제목도 굵은 청록색으로 표시합니다. 파일·파이프 등으로 출력을 리다이렉트하거나
|
|
18
|
+
`NO_COLOR`를 설정하면 ANSI 색상 코드를 출력하지 않습니다.
|
|
19
|
+
- `Worker.run()`의 로그와 배너 호출 순서는 변경하지 않았습니다. 따라서
|
|
20
|
+
로깅 핸들러가 미리 설정된 애플리케이션에서는 `Starting Worker` 로그가 배너보다
|
|
21
|
+
먼저 기록될 수 있습니다. 이 변경은 샘플의 로깅 설정 시점을 조정해 샘플 출력에서
|
|
22
|
+
배너를 먼저 보이게 합니다.
|
|
23
|
+
|
|
24
|
+
## 호환성 및 검증 범위
|
|
25
|
+
|
|
26
|
+
시작 배너의 ASCII 문자와 구성 출력은 유지됩니다. 색상은 터미널 조건에서만
|
|
27
|
+
추가되며 `NO_COLOR`를 통해 끌 수 있습니다.
|
|
28
|
+
|
|
29
|
+
로고 색상 적용 후 ANSI 코드를 제거한 결과가 원본과 동일한지, 수정한 Python 소스의
|
|
30
|
+
문법이 유효한지 확인했습니다. Worker 시작 테스트는 환경에 `pika`가 없어 실행하지
|
|
31
|
+
못했으며, 샘플의 로깅 순서 변경은 별도 실행으로 검증하지 않았습니다.
|
|
@@ -3,7 +3,7 @@ from random import randint
|
|
|
3
3
|
from Tasks import (
|
|
4
4
|
Add,
|
|
5
5
|
Divide,
|
|
6
|
-
|
|
6
|
+
TaskLoggingSetup,
|
|
7
7
|
logger,
|
|
8
8
|
)
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ from DefStream.Worker import request
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
if __name__ == '__main__':
|
|
19
|
-
|
|
19
|
+
TaskLoggingSetup()()
|
|
20
20
|
broker = Connection(Configuration(
|
|
21
21
|
host='127.0.0.1',
|
|
22
22
|
port=5672,
|
|
@@ -28,9 +28,11 @@ from DefStream.Validator.Patterns.Object import (
|
|
|
28
28
|
IsRequiredIn,
|
|
29
29
|
)
|
|
30
30
|
from DefStream.Worker import (
|
|
31
|
+
WorkerSetup,
|
|
31
32
|
PoolSetup,
|
|
32
33
|
Run,
|
|
33
34
|
Specification,
|
|
35
|
+
Worker,
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
__all__ = (
|
|
@@ -42,7 +44,27 @@ __all__ = (
|
|
|
42
44
|
logger = getLogger('Sample')
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
class
|
|
47
|
+
class SetupLogger(WorkerSetup):
|
|
48
|
+
"""부모와 spawn 자식 프로세스의 샘플 로그 출력을 설정한다."""
|
|
49
|
+
|
|
50
|
+
def __call__(
|
|
51
|
+
self,
|
|
52
|
+
worker: Worker,
|
|
53
|
+
) -> None:
|
|
54
|
+
handler = StreamHandler()
|
|
55
|
+
handler.setFormatter(Formatter('%(asctime)s %(processName)s %(name)s %(levelname)s %(message)s'))
|
|
56
|
+
sources = (
|
|
57
|
+
Filter('Sample'),
|
|
58
|
+
Filter('DefStream'),
|
|
59
|
+
)
|
|
60
|
+
handler.addFilter(lambda record: any(source.filter(record) for source in sources))
|
|
61
|
+
root = getLogger()
|
|
62
|
+
root.setLevel(DEBUG)
|
|
63
|
+
root.addHandler(handler)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TaskLoggingSetup(PoolSetup):
|
|
46
68
|
"""부모와 spawn 자식 프로세스의 샘플 로그 출력을 설정한다."""
|
|
47
69
|
|
|
48
70
|
def __call__(self) -> None:
|
|
@@ -7,7 +7,8 @@ from signal import (
|
|
|
7
7
|
from Tasks import (
|
|
8
8
|
Add,
|
|
9
9
|
Divide,
|
|
10
|
-
|
|
10
|
+
TaskLoggingSetup,
|
|
11
|
+
SetupLogger,
|
|
11
12
|
logger,
|
|
12
13
|
)
|
|
13
14
|
|
|
@@ -76,7 +77,6 @@ class Cleanup(WorkerCleanup):
|
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
if __name__ == '__main__':
|
|
79
|
-
LoggingSetup()()
|
|
80
80
|
worker = Worker(
|
|
81
81
|
Connection(Configuration(
|
|
82
82
|
host='127.0.0.1',
|
|
@@ -86,9 +86,12 @@ if __name__ == '__main__':
|
|
|
86
86
|
)),
|
|
87
87
|
ProcessPool(
|
|
88
88
|
10,
|
|
89
|
-
setups=
|
|
89
|
+
setups=TaskLoggingSetup(),
|
|
90
|
+
),
|
|
91
|
+
setups=(
|
|
92
|
+
SetupLogger(),
|
|
93
|
+
Prepare(),
|
|
90
94
|
),
|
|
91
|
-
setups=Prepare(),
|
|
92
95
|
cleanups=Cleanup(),
|
|
93
96
|
inspector=Inspector(
|
|
94
97
|
host='127.0.0.1',
|
|
@@ -39,19 +39,6 @@ from DefStream.Worker import (
|
|
|
39
39
|
logger = getLogger('Sample')
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def configure_logging() -> None:
|
|
43
|
-
handler = StreamHandler()
|
|
44
|
-
handler.setFormatter(Formatter('%(asctime)s %(name)s %(levelname)s %(message)s'))
|
|
45
|
-
sources = (
|
|
46
|
-
Filter('Sample'),
|
|
47
|
-
Filter('DefStream'),
|
|
48
|
-
)
|
|
49
|
-
handler.addFilter(lambda record: any(source.filter(record) for source in sources))
|
|
50
|
-
root = getLogger()
|
|
51
|
-
root.setLevel(DEBUG)
|
|
52
|
-
root.addHandler(handler)
|
|
53
|
-
return
|
|
54
|
-
|
|
55
42
|
|
|
56
43
|
class Add(
|
|
57
44
|
Run,
|
|
@@ -96,6 +83,24 @@ class Divide(
|
|
|
96
83
|
return result
|
|
97
84
|
|
|
98
85
|
|
|
86
|
+
class SetupLogger(WorkerSetup):
|
|
87
|
+
def __call__(
|
|
88
|
+
self,
|
|
89
|
+
worker: Worker,
|
|
90
|
+
) -> None:
|
|
91
|
+
handler = StreamHandler()
|
|
92
|
+
handler.setFormatter(Formatter('%(asctime)s %(name)s %(levelname)s %(message)s'))
|
|
93
|
+
sources = (
|
|
94
|
+
Filter('Sample'),
|
|
95
|
+
Filter('DefStream'),
|
|
96
|
+
)
|
|
97
|
+
handler.addFilter(lambda record: any(source.filter(record) for source in sources))
|
|
98
|
+
root = getLogger()
|
|
99
|
+
root.setLevel(DEBUG)
|
|
100
|
+
root.addHandler(handler)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
|
|
99
104
|
class Prepare(WorkerSetup):
|
|
100
105
|
def __call__(
|
|
101
106
|
self,
|
|
@@ -139,11 +144,13 @@ class Cleanup(WorkerCleanup):
|
|
|
139
144
|
|
|
140
145
|
|
|
141
146
|
def main() -> None:
|
|
142
|
-
configure_logging()
|
|
143
147
|
worker = Worker(
|
|
144
148
|
Connection(Configuration()),
|
|
145
149
|
ThreadPool(10),
|
|
146
|
-
setups=
|
|
150
|
+
setups=(
|
|
151
|
+
SetupLogger(),
|
|
152
|
+
Prepare(),
|
|
153
|
+
),
|
|
147
154
|
cleanups=Cleanup(),
|
|
148
155
|
inspector=Inspector(
|
|
149
156
|
host='127.0.0.1',
|
|
@@ -35,19 +35,6 @@ from DefStream.Worker import (
|
|
|
35
35
|
logger = getLogger('Sample')
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def configure_logging() -> None:
|
|
39
|
-
handler = StreamHandler()
|
|
40
|
-
handler.setFormatter(Formatter('%(asctime)s %(name)s %(levelname)s %(message)s'))
|
|
41
|
-
sources = (
|
|
42
|
-
Filter('Sample'),
|
|
43
|
-
Filter('DefStream'),
|
|
44
|
-
)
|
|
45
|
-
handler.addFilter(lambda record: any(source.filter(record) for source in sources))
|
|
46
|
-
root = getLogger()
|
|
47
|
-
root.setLevel(DEBUG)
|
|
48
|
-
root.addHandler(handler)
|
|
49
|
-
return
|
|
50
|
-
|
|
51
38
|
|
|
52
39
|
class Queue(Sendable):
|
|
53
40
|
def __init__(self):
|
|
@@ -219,6 +206,24 @@ class Add(
|
|
|
219
206
|
)
|
|
220
207
|
|
|
221
208
|
|
|
209
|
+
class SetupLogger(WorkerSetup):
|
|
210
|
+
def __call__(
|
|
211
|
+
self,
|
|
212
|
+
worker: Worker,
|
|
213
|
+
) -> None:
|
|
214
|
+
handler = StreamHandler()
|
|
215
|
+
handler.setFormatter(Formatter('%(asctime)s %(name)s %(levelname)s %(message)s'))
|
|
216
|
+
sources = (
|
|
217
|
+
Filter('Sample'),
|
|
218
|
+
Filter('DefStream'),
|
|
219
|
+
)
|
|
220
|
+
handler.addFilter(lambda record: any(source.filter(record) for source in sources))
|
|
221
|
+
root = getLogger()
|
|
222
|
+
root.setLevel(DEBUG)
|
|
223
|
+
root.addHandler(handler)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
|
|
222
227
|
class Prepare(WorkerSetup):
|
|
223
228
|
def __call__(
|
|
224
229
|
self,
|
|
@@ -252,12 +257,16 @@ class Cleanup(WorkerCleanup):
|
|
|
252
257
|
|
|
253
258
|
|
|
254
259
|
if __name__ == '__main__':
|
|
255
|
-
configure_logging()
|
|
256
260
|
worker = Worker(
|
|
257
261
|
Broker(),
|
|
258
262
|
ThreadPool(1),
|
|
259
|
-
setups=
|
|
260
|
-
|
|
263
|
+
setups=(
|
|
264
|
+
SetupLogger(),
|
|
265
|
+
Prepare(),
|
|
266
|
+
),
|
|
267
|
+
cleanups=(
|
|
268
|
+
Cleanup(),
|
|
269
|
+
),
|
|
261
270
|
)
|
|
262
271
|
worker.add(Add)
|
|
263
272
|
worker.run()
|
|
@@ -4,8 +4,8 @@ from abc import (
|
|
|
4
4
|
abstractmethod,
|
|
5
5
|
)
|
|
6
6
|
from collections.abc import Mapping
|
|
7
|
-
from keyword import iskeyword
|
|
8
7
|
from logging import getLogger
|
|
8
|
+
from re import fullmatch
|
|
9
9
|
from time import monotonic
|
|
10
10
|
from typing import (
|
|
11
11
|
Any,
|
|
@@ -138,8 +138,11 @@ class Run(ABC, metaclass=RunMeta):
|
|
|
138
138
|
str,
|
|
139
139
|
):
|
|
140
140
|
raise TypeError('Run name must be a string or None')
|
|
141
|
-
if run_name is not None and (
|
|
142
|
-
|
|
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')
|
|
143
146
|
if description is not None and not isinstance(
|
|
144
147
|
description,
|
|
145
148
|
str,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
|
-
from os import getpid
|
|
2
|
+
from os import environ, getpid
|
|
3
|
+
import sys
|
|
3
4
|
from typing import (
|
|
4
5
|
Any,
|
|
5
6
|
TYPE_CHECKING,
|
|
@@ -19,9 +20,42 @@ LOGO = r"""
|
|
|
19
20
|
███ ███ ▄█▀█▄ ▀██▀ ▀████▄ ▀██▀▀ ████▄ ▄█▀█▄ ▀▀█▄ ███▄███▄
|
|
20
21
|
███ ███ ██▄█▀ ██ ▀████ ██ ██ ▀▀ ██▄█▀ ▄█▀██ ██ ██ ██
|
|
21
22
|
██████▀ ▀█▄▄▄ ██ ███████▀ ██ ██ ▀█▄▄▄ ▀█▄██ ██ ██ ██
|
|
23
|
+
|
|
24
|
+
Just def. Let workers do the work. You don't need to await this.
|
|
22
25
|
"""
|
|
23
26
|
|
|
24
27
|
|
|
28
|
+
def supports_color() -> bool:
|
|
29
|
+
return (
|
|
30
|
+
getattr(sys.stdout, 'isatty', lambda: False)()
|
|
31
|
+
and not environ.get('NO_COLOR')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def colorize_logo(logo: str) -> str:
|
|
36
|
+
colors = (
|
|
37
|
+
'\033[38;5;39m',
|
|
38
|
+
'\033[38;5;45m',
|
|
39
|
+
'\033[38;5;51m',
|
|
40
|
+
'\033[38;5;45m',
|
|
41
|
+
'\033[38;5;39m',
|
|
42
|
+
)
|
|
43
|
+
reset = '\033[0m'
|
|
44
|
+
colorized_lines = []
|
|
45
|
+
art_line = 0
|
|
46
|
+
for line in logo.splitlines(keepends=True):
|
|
47
|
+
if line.strip() and art_line < len(colors):
|
|
48
|
+
newline = '\n' if line.endswith('\n') else ''
|
|
49
|
+
colorized_lines.append(
|
|
50
|
+
f'{colors[art_line]}{line[:-1] if newline else line}'
|
|
51
|
+
f'{reset}{newline}',
|
|
52
|
+
)
|
|
53
|
+
art_line += 1
|
|
54
|
+
else:
|
|
55
|
+
colorized_lines.append(line)
|
|
56
|
+
return ''.join(colorized_lines)
|
|
57
|
+
|
|
58
|
+
|
|
25
59
|
def subscription_value(
|
|
26
60
|
parameters: Any,
|
|
27
61
|
name: str,
|
|
@@ -90,10 +124,15 @@ def startup(worker: 'Worker') -> None:
|
|
|
90
124
|
str,
|
|
91
125
|
) and vhost:
|
|
92
126
|
broker_name += '/' + vhost.lstrip('/')
|
|
127
|
+
logo = LOGO.rstrip()
|
|
128
|
+
starting_worker = 'Starting Worker'
|
|
129
|
+
if supports_color():
|
|
130
|
+
logo = colorize_logo(logo)
|
|
131
|
+
starting_worker = f'\033[1;38;5;51m{starting_worker}\033[0m'
|
|
93
132
|
lines = [
|
|
94
|
-
|
|
133
|
+
logo,
|
|
95
134
|
'',
|
|
96
|
-
|
|
135
|
+
starting_worker,
|
|
97
136
|
f' PID: {getpid()}',
|
|
98
137
|
f' Broker: {broker_name}',
|
|
99
138
|
]
|
|
@@ -498,7 +498,7 @@ class InspectorTests(TestCase):
|
|
|
498
498
|
class NamedAdd(
|
|
499
499
|
Add,
|
|
500
500
|
parameters={'queue': 'named-add'},
|
|
501
|
-
name='
|
|
501
|
+
name='Orders.create-v2_Job',
|
|
502
502
|
):
|
|
503
503
|
pass
|
|
504
504
|
|
|
@@ -514,7 +514,21 @@ class InspectorTests(TestCase):
|
|
|
514
514
|
self.addCleanup(worker.pool.stop)
|
|
515
515
|
self.assertEqual(
|
|
516
516
|
set(metrics.tasks),
|
|
517
|
-
{'
|
|
517
|
+
{'Orders.create-v2_Job'},
|
|
518
|
+
)
|
|
519
|
+
started = metrics.begin(NamedAdd.name)
|
|
520
|
+
metrics.end(
|
|
521
|
+
NamedAdd.name,
|
|
522
|
+
started,
|
|
523
|
+
True,
|
|
524
|
+
)
|
|
525
|
+
self.assertEqual(
|
|
526
|
+
metrics.snapshot(inspector)['tasks'][NamedAdd.name]['success'],
|
|
527
|
+
1,
|
|
528
|
+
)
|
|
529
|
+
self.assertIn(
|
|
530
|
+
'defstream_tasks_total{task="Orders.create-v2_Job",outcome="success"} 1',
|
|
531
|
+
metrics.render(inspector),
|
|
518
532
|
)
|
|
519
533
|
|
|
520
534
|
def test_metrics_reject_duplicate_run_names_without_overwriting(self):
|
|
@@ -426,7 +426,7 @@ class DocumentTests(TestCase):
|
|
|
426
426
|
class Described(
|
|
427
427
|
Add,
|
|
428
428
|
parameters={'queue': 'described'},
|
|
429
|
-
name='
|
|
429
|
+
name='Orders.create-v2_Job',
|
|
430
430
|
description='Add request description',
|
|
431
431
|
):
|
|
432
432
|
"""Not the operation description."""
|
|
@@ -444,7 +444,7 @@ class DocumentTests(TestCase):
|
|
|
444
444
|
inspector.worker.add(Empty)
|
|
445
445
|
operations = Specifications('/spec').document(inspector)['operations']
|
|
446
446
|
self.assertEqual(
|
|
447
|
-
operations['
|
|
447
|
+
operations['Orders.create-v2_Job']['description'],
|
|
448
448
|
'Add request description',
|
|
449
449
|
)
|
|
450
450
|
self.assertEqual(
|
|
@@ -469,12 +469,42 @@ class DocumentTests(TestCase):
|
|
|
469
469
|
Local.__module__ + '.' + Local.__qualname__,
|
|
470
470
|
)
|
|
471
471
|
|
|
472
|
-
def
|
|
472
|
+
def test_run_names_preserve_allowed_characters(self):
|
|
473
|
+
for name in (
|
|
474
|
+
'Orders.create-v2_Job',
|
|
475
|
+
'123',
|
|
476
|
+
'class',
|
|
477
|
+
'.-_',
|
|
478
|
+
'-Job.',
|
|
479
|
+
):
|
|
480
|
+
with self.subTest(name=name):
|
|
481
|
+
class Named(
|
|
482
|
+
Run,
|
|
483
|
+
name=name,
|
|
484
|
+
):
|
|
485
|
+
def run(self):
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
self.assertEqual(
|
|
489
|
+
Named.name,
|
|
490
|
+
name,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def test_run_names_reject_empty_strings_and_unsupported_characters(self):
|
|
473
494
|
for invalid in (
|
|
474
495
|
'',
|
|
475
|
-
'not-valid',
|
|
476
496
|
'has space',
|
|
477
|
-
'
|
|
497
|
+
' leading',
|
|
498
|
+
'trailing ',
|
|
499
|
+
'job\n',
|
|
500
|
+
'job\t',
|
|
501
|
+
'job/name',
|
|
502
|
+
'job:name',
|
|
503
|
+
'job"name',
|
|
504
|
+
'job\\name',
|
|
505
|
+
'\uC791\uC5C5',
|
|
506
|
+
'caf\u00e9',
|
|
507
|
+
'\uff11',
|
|
478
508
|
):
|
|
479
509
|
with self.subTest(name=invalid), self.assertRaises(ValueError):
|
|
480
510
|
class Invalid(
|
|
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
|
|
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
|
|
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
|
|
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
|